From 43d5ac070df39628aaae531fd5928136594b2fd6 Mon Sep 17 00:00:00 2001 From: Ernesto Rico Schmidt <ernesto@helsinki.at> Date: Tue, 4 Apr 2023 16:08:17 -0400 Subject: [PATCH] Extract generate_timeslots as function --- program/models.py | 152 +------------------------------------------- program/services.py | 149 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 149 insertions(+), 152 deletions(-) diff --git a/program/models.py b/program/models.py index 0fafc80b..e2f317fa 100644 --- a/program/models.py +++ b/program/models.py @@ -18,10 +18,8 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from datetime import datetime, timedelta +from datetime import datetime -from dateutil.relativedelta import relativedelta -from dateutil.rrule import rrule from rest_framework.exceptions import ValidationError from versatileimagefield.fields import PPOIField, VersatileImageField @@ -29,7 +27,6 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.db.models import Q, QuerySet -from django.utils import timezone from django.utils.translation import gettext_lazy as _ from program.utils import parse_datetime @@ -375,153 +372,6 @@ class Schedule(models.Model): class Meta: ordering = ("first_date", "start_time") - # FIXME: this does not belong here - @staticmethod - def generate_timeslots(schedule): - """ - Returns a list of timeslot objects based on a schedule and its rrule - Returns past timeslots as well, starting from first_date (not today) - """ - timeslots = [] - - # adjust last_date if end_time is after midnight - if schedule.end_time < schedule.start_time: - last_date = schedule.first_date + timedelta(days=+1) - else: - last_date = schedule.first_date - - if schedule.rrule.freq == 3: # daily: Ignore schedule.by_weekday to set by_weekday - by_weekday_start = by_weekday_end = (0, 1, 2, 3, 4, 5, 6) - elif ( - schedule.rrule.freq == 2 - and schedule.rrule.interval == 1 - and schedule.rrule.by_weekdays is None - ): # weekly: Use schedule.by_weekday for by_weekday - by_weekday_start = by_weekday_end = int(schedule.by_weekday) - - # adjust by_weekday_end if end_time is after midnight - if schedule.end_time < schedule.start_time: - by_weekday_end = by_weekday_start + 1 if by_weekday_start < 6 else 0 - elif ( - schedule.rrule.freq == 2 - and schedule.rrule.interval == 1 - and schedule.rrule.by_weekdays == "0,1,2,3,4" - ): # weekly on business days: Use schedule.rrule.by_weekdays to set by_weekday - by_weekday_start = by_weekday_end = [ - int(wd) for wd in schedule.rrule.by_weekdays.split(",") - ] - - # adjust by_weekday_end if end_time is after midnight - if schedule.end_time < schedule.start_time: - by_weekday_end = (1, 2, 3, 4, 5) - elif ( - schedule.rrule.freq == 2 - and schedule.rrule.interval == 1 - and schedule.rrule.by_weekdays == "5,6" - ): # weekly on weekends: Use schedule.rrule.by_weekdays to set by_weekday - by_weekday_start = by_weekday_end = [ - int(wd) for wd in schedule.rrule.by_weekdays.split(",") - ] - - # adjust by_weekday_end if end_time is after midnight - if schedule.end_time < schedule.start_time: - by_weekday_end = (6, 0) - elif schedule.rrule.freq == 0: # once: Ignore schedule.by_weekday to set by_weekday - by_weekday_start = by_weekday_end = None - else: - by_weekday_start = by_weekday_end = ( - int(schedule.by_weekday) if schedule.by_weekday is not None else None - ) - - # adjust by_weekday_end if end_time is after midnight - if schedule.end_time < schedule.start_time: - by_weekday_end = by_weekday_start + 1 if by_weekday_start < 6 else 0 - - if schedule.rrule.freq == 0: # once: - starts = [datetime.combine(schedule.first_date, schedule.start_time)] - ends = [datetime.combine(last_date, schedule.end_time)] - else: - starts = list( - rrule( - freq=schedule.rrule.freq, - dtstart=datetime.combine(schedule.first_date, schedule.start_time), - interval=schedule.rrule.interval, - until=schedule.last_date + relativedelta(days=+1), - bysetpos=schedule.rrule.by_set_pos, - byweekday=by_weekday_start, - ) - ) - ends = list( - rrule( - freq=schedule.rrule.freq, - dtstart=datetime.combine(last_date, schedule.end_time), - interval=schedule.rrule.interval, - until=schedule.last_date + relativedelta(days=+1), - bysetpos=schedule.rrule.by_set_pos, - byweekday=by_weekday_end, - ) - ) - - for k in range(min(len(starts), len(ends))): - # Correct dates for the (relatively seldom) case if: - # E.g.: 1st Monday from 23:00:00 to 1st Tuesday 00:00:00 - # produces wrong end dates if the 1st Tuesday is before the 1st Monday - # In this case we take the next day instead of rrule's calculated end - if starts[k] > ends[k]: - ends[k] = datetime.combine(starts[k] + relativedelta(days=+1), schedule.end_time) - - """ - Add a number of days to the generated dates? - - This can be helpful for repetitions: - - Examples: - - 1. If RRule is "Every 1st Monday" and we want its repetition always to be on the - following day, the repetition's RRule is the same but add_days_no is 1 - - If we would set the repetition to "Every 1st Tuesday" instead - we will get unmeant results if the 1st Tuesday is before the 1st Monday - (e.g. 1st Tue = May 1 2018, 1st Mon = May 7 2018) - - 2. If RRule is "Every 1st Friday" and we want its repetition always to be on the - following business day, the repetition's RRule is the same but add_days_no is 1 - and add_business_days_only is True (e.g. original date = Fri, March 2 2018; - generated date = Mon, March 5 2018) - - In the UI these can be presets: - "On the following day" (add_days_no=1,add_business_days_only=False) or - "On the following business day" (add_days_no=1,add_business_days_only=True) - - """ - if schedule.add_days_no is not None and schedule.add_days_no > 0: - # If only business days and weekday is Fri, Sat or Sun: add add_days_no beginning - # from Sunday - weekday = datetime.date(starts[k]).weekday() - if schedule.add_business_days_only and weekday > 3: - days_until_sunday = 6 - weekday - starts[k] = starts[k] + relativedelta( - days=+days_until_sunday + schedule.add_days_no - ) - ends[k] = ends[k] + relativedelta( - days=+days_until_sunday + schedule.add_days_no - ) - else: - starts[k] = starts[k] + relativedelta(days=+schedule.add_days_no) - ends[k] = ends[k] + relativedelta(days=+schedule.add_days_no) - - if ends[k].date() > schedule.last_date: - schedule.last_date = ends[k].date() - timeslots.append( - TimeSlot( - schedule=schedule, - start=timezone.make_aware(starts[k], is_dst=True), - end=timezone.make_aware(ends[k], is_dst=True), - ) - ) - - return timeslots - # FIXME: this does not belong here @staticmethod def generate_conflicts(timeslots): diff --git a/program/services.py b/program/services.py index a6d509df..09dc7295 100644 --- a/program/services.py +++ b/program/services.py @@ -19,6 +19,8 @@ from datetime import datetime, time, timedelta +from dateutil.relativedelta import relativedelta +from dateutil.rrule import rrule from rest_framework.exceptions import ValidationError from django.core.exceptions import ObjectDoesNotExist @@ -391,10 +393,155 @@ def make_conflicts(sdl, schedule_pk, show_pk): ) gen_schedule.first_date = last_timeslot.start.date() + timedelta(days=1) - timeslots = Schedule.generate_timeslots(gen_schedule) + timeslots = generate_timeslots(gen_schedule) # Generate conflicts and add schedule conflicts = Schedule.generate_conflicts(timeslots) conflicts["schedule"] = model_to_dict(schedule) return conflicts + + +# TODO: add type annotations +def generate_timeslots(schedule): + """ + Returns a list of timeslot objects based on a schedule and its rrule + Returns past timeslots as well, starting from first_date (not today) + """ + timeslots = [] + + # adjust last_date if end_time is after midnight + if schedule.end_time < schedule.start_time: + last_date = schedule.first_date + timedelta(days=+1) + else: + last_date = schedule.first_date + + if schedule.rrule.freq == 3: # daily: Ignore schedule.by_weekday to set by_weekday + by_weekday_start = by_weekday_end = (0, 1, 2, 3, 4, 5, 6) + elif ( + schedule.rrule.freq == 2 + and schedule.rrule.interval == 1 + and schedule.rrule.by_weekdays is None + ): # weekly: Use schedule.by_weekday for by_weekday + by_weekday_start = by_weekday_end = int(schedule.by_weekday) + + # adjust by_weekday_end if end_time is after midnight + if schedule.end_time < schedule.start_time: + by_weekday_end = by_weekday_start + 1 if by_weekday_start < 6 else 0 + elif ( + schedule.rrule.freq == 2 + and schedule.rrule.interval == 1 + and schedule.rrule.by_weekdays == "0,1,2,3,4" + ): # weekly on business days: Use schedule.rrule.by_weekdays to set by_weekday + by_weekday_start = by_weekday_end = [ + int(wd) for wd in schedule.rrule.by_weekdays.split(",") + ] + + # adjust by_weekday_end if end_time is after midnight + if schedule.end_time < schedule.start_time: + by_weekday_end = (1, 2, 3, 4, 5) + elif ( + schedule.rrule.freq == 2 + and schedule.rrule.interval == 1 + and schedule.rrule.by_weekdays == "5,6" + ): # weekly on weekends: Use schedule.rrule.by_weekdays to set by_weekday + by_weekday_start = by_weekday_end = [ + int(wd) for wd in schedule.rrule.by_weekdays.split(",") + ] + + # adjust by_weekday_end if end_time is after midnight + if schedule.end_time < schedule.start_time: + by_weekday_end = (6, 0) + elif schedule.rrule.freq == 0: # once: Ignore schedule.by_weekday to set by_weekday + by_weekday_start = by_weekday_end = None + else: + by_weekday_start = by_weekday_end = ( + int(schedule.by_weekday) if schedule.by_weekday is not None else None + ) + + # adjust by_weekday_end if end_time is after midnight + if schedule.end_time < schedule.start_time: + by_weekday_end = by_weekday_start + 1 if by_weekday_start < 6 else 0 + + if schedule.rrule.freq == 0: # once: + starts = [datetime.combine(schedule.first_date, schedule.start_time)] + ends = [datetime.combine(last_date, schedule.end_time)] + else: + starts = list( + rrule( + freq=schedule.rrule.freq, + dtstart=datetime.combine(schedule.first_date, schedule.start_time), + interval=schedule.rrule.interval, + until=schedule.last_date + relativedelta(days=+1), + bysetpos=schedule.rrule.by_set_pos, + byweekday=by_weekday_start, + ) + ) + ends = list( + rrule( + freq=schedule.rrule.freq, + dtstart=datetime.combine(last_date, schedule.end_time), + interval=schedule.rrule.interval, + until=schedule.last_date + relativedelta(days=+1), + bysetpos=schedule.rrule.by_set_pos, + byweekday=by_weekday_end, + ) + ) + + for k in range(min(len(starts), len(ends))): + # Correct dates for the (relatively seldom) case if: + # E.g.: 1st Monday from 23:00:00 to 1st Tuesday 00:00:00 + # produces wrong end dates if the 1st Tuesday is before the 1st Monday + # In this case we take the next day instead of rrule's calculated end + if starts[k] > ends[k]: + ends[k] = datetime.combine(starts[k] + relativedelta(days=+1), schedule.end_time) + + """ + Add a number of days to the generated dates? + + This can be helpful for repetitions: + + Examples: + + 1. If RRule is "Every 1st Monday" and we want its repetition always to be on the + following day, the repetition's RRule is the same but add_days_no is 1 + + If we would set the repetition to "Every 1st Tuesday" instead + we will get unmeant results if the 1st Tuesday is before the 1st Monday + (e.g. 1st Tue = May 1 2018, 1st Mon = May 7 2018) + + 2. If RRule is "Every 1st Friday" and we want its repetition always to be on the + following business day, the repetition's RRule is the same but add_days_no is 1 + and add_business_days_only is True (e.g. original date = Fri, March 2 2018; + generated date = Mon, March 5 2018) + + In the UI these can be presets: + "On the following day" (add_days_no=1,add_business_days_only=False) or + "On the following business day" (add_days_no=1,add_business_days_only=True) + + """ + if schedule.add_days_no is not None and schedule.add_days_no > 0: + # If only business days and weekday is Fri, Sat or Sun: add add_days_no beginning + # from Sunday + weekday = datetime.date(starts[k]).weekday() + if schedule.add_business_days_only and weekday > 3: + days_until_sunday = 6 - weekday + starts[k] = starts[k] + relativedelta( + days=+days_until_sunday + schedule.add_days_no + ) + ends[k] = ends[k] + relativedelta(days=+days_until_sunday + schedule.add_days_no) + else: + starts[k] = starts[k] + relativedelta(days=+schedule.add_days_no) + ends[k] = ends[k] + relativedelta(days=+schedule.add_days_no) + + if ends[k].date() > schedule.last_date: + schedule.last_date = ends[k].date() + timeslots.append( + TimeSlot( + schedule=schedule, + start=timezone.make_aware(starts[k], is_dst=True), + end=timezone.make_aware(ends[k], is_dst=True), + ) + ) + + return timeslots -- GitLab