Skip to content

Refactor calculation of recurring events #2094

Description

@timobrembeck

Motivation

At the moment, the calculation of event recurrences is spread over the

  • events API:
    def transform_event_recurrences(event_translation, today):
    """
    Yield all future recurrences of the event.
    :param event_translation: The event translation object which should be converted
    :type event_translation: ~integreat_cms.cms.models.events.event_translation.EventTranslation
    :param today: The first date at which event may be yielded
    :type today: ~datetime.date
    :return: An iterator over all future recurrences up to ``settings.API_EVENTS_MAX_TIME_SPAN_DAYS``
    :rtype: Iterator[:class:`~datetime.date`]
    """
    event = event_translation.event
    recurrence_rule = event.recurrence_rule
    if not recurrence_rule:
    return
    event_is_invalid = (
    recurrence_rule.recurrence_end_date
    and recurrence_rule.recurrence_end_date < today
    )
    if event_is_invalid:
    return
    start_date = event.start_local.date()
    event_translation.id = None
    # Calculate all recurrences of this event
    for recurrence_date in recurrence_rule.iter_after(start_date):
    if recurrence_date - max(start_date, today) > timedelta(
    days=settings.API_EVENTS_MAX_TIME_SPAN_DAYS
    ):
    break
    if recurrence_date < today or recurrence_date == start_date:
    continue
    yield transform_event_translation(event_translation, recurrence_date)
  • Event model:
    def get_occurrences(self, start, end):
    """
    Get occurrences of the event that overlap with ``[start, end]``.
    Expects ``start < end``.
    :param start: the begin of the requested interval.
    :type start: ~datetime.datetime
    :param end: the end of the requested interval.
    :type end: ~datetime.datetime
    :return: start datetimes of occurrences of the event that are in the given timeframe
    :rtype: list [ ~datetime.datetime ]
    """
    event_start = self.start
    event_end = self.end
    if self.is_recurring is not None:
    return self.recurrence_rule.to_ical_rrule().between(start, end)
    return (
    [event_start]
    if start <= event_start <= end or start <= event_end <= end
    else []
    )
  • RecurrenceRule model:
    def advance():
    """
    Get the next occurrence by this rule
    :return: date objects
    :rtype: Iterator[:class: `~datetime.date`]
    """
    nonlocal next_recurrence
    if self.frequency == frequency.DAILY:
    yield
    next_recurrence += timedelta(days=1)
    elif self.frequency == frequency.WEEKLY:
    # Yield each day of the week that is valid, since
    # ``interval`` should apply here only on weekly basis
    for weekday in sorted(self.weekdays_for_weekly):
    if weekday < next_recurrence.weekday():
    continue
    next_recurrence += timedelta(
    days=weekday - next_recurrence.weekday()
    )
    yield
    # advance to the next monday
    next_recurrence += timedelta(days=7 - next_recurrence.weekday())
    elif self.frequency == frequency.MONTHLY:
    next_recurrence = get_nth_weekday(
    next_recurrence, self.weekday_for_monthly, self.week_for_monthly
    )
    if next_recurrence < start_date:
    next_recurrence = get_nth_weekday(
    next_month(next_recurrence),
    self.weekday_for_monthly,
    self.week_for_monthly,
    )
    yield
    next_recurrence = next_month(next_recurrence)
    elif self.frequency == frequency.YEARLY:
    yield
    # It is not possible to go simply to the next year if the current date is february 29
    year_dif = 1
    while True:
    try:
    next_recurrence = next_recurrence.replace(
    year=next_recurrence.year + year_dif
    )
    break
    except ValueError:
    year_dif += 1

Proposed Solution

Refactor the code so it

  1. Isn't spread over different modules anymore (probably the recurrence rule model is the most sensible place for it)
  2. Uses the iCal rrule to calculate the recurrences instead of the custom function

Additional Context

Also, there is a small bug currently that recurring events which spread over multiple days are not shown in the API when the occurrence is ongoing but started e.g. yesterday.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort: mediumShould be doable in <12hfeature:EventsIssues related to the events moduleprio: lowNot urgent, can be resolved in the distant future.readyThis issue is ready to be worked ontopic:RefactoringIssues related to refactoring

    Type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions