Repository: ansible/awx
File: awx/main/models/schedules.py (Python)

Code before:

    @classmethod
    def rrulestr(cls, rrule, fast_forward=True, **kwargs):
        """
        Apply our own custom rrule parsing requirements
        """
        rrule = Schedule.coerce_naive_until(rrule)
        kwargs['forceset'] = True
        x = dateutil.rrule.rrulestr(rrule, tzinfos=UTC_TIMEZONES, **kwargs)

        for r in x._rrule:
            if r._dtstart and r._dtstart.tzinfo is None:
                raise ValueError('A valid TZID must be provided (e.g., America/New_York)')

        # Fast forward is a way for us to limit the number of events in the rruleset
        # If we are fast forwarding and we don't have a count limited rule that is minutely or hourly
        # We will modify the start date of the rule to last week to prevent a large number of entries
        if fast_forward:
            try:
                # All rules in a ruleset will have the same dtstart value
                # so lets compare the first event to now to see if its > 30 days old
                # we choose > 30 days because if rrule has FREQ=HOURLY and INTERVAL=23, it will take
                # at least 23 days before we can fast forward reliably and retain stable occurrences.
                # since we are fast forwarding to 7 days ago, we check fo rrrules older than (23+7) days
                first_event = x[0]
                if (now() - first_event).days > 30:
                    for rule in x._rrule:
                        # If any rule has a minutely or hourly rule without a count...
                        if rule._freq in [dateutil.rrule.MINUTELY, dateutil.rrule.HOURLY] and not rule._count:
                            # hourly/minutely rrules with far-past DTSTART values
                            # are *really* slow to precompute
                            # start *from* one week ago to speed things up drastically
                            try:
                                new_start = fast_forward_date(rule)
                            except RuntimeError as e:
                                logger.warning(e)
                                # fallback to setting dtstart to 7 days ago, but this has the consequence of
                                # occurrences not matching the old occurrences.
                                new_start = now() - datetime.timedelta(days=7)
                            new_start_fmt = new_start.strftime('%Y%m%d')
                            # Now we want to replace the DTSTART:<value>T with the new date (which includes the T)
                            new_rrule = re.sub('(DTSTART[^:]*):[^T]+T', r'\1:{0}T'.format(new_start_fmt), rrule)
                            return Schedule.rrulestr(new_rrule, fast_forward=False)
            except IndexError:
                pass

        return x

    def __str__(self):
        return u'%s_t%s_%s_%s' % (self.name, self.unified_job_template.id, self.id, self.next_run)


Review comment (suggestion):
May I suggest moving the arbitrary constant of `30` into a variable? Maybe, a class/module-level constant. This piece of data needs a name.

Code after:
                    # We are going to construct a 'dummy' rule for parsing which will include the DTSTART and the rest of the rule
                    temp_rule = "{} {}".format(start_date_rule, rule.replace(naive_until, naive_until + 'Z'))
                    # If the rule is an EX rule we have to add an RRULE to it because an EX rule alone will not manifest into a ruleset
                    if rule.lower().startswith('ex'):
                        temp_rule = "{} {}".format(temp_rule, 'RRULE:FREQ=MINUTELY;INTERVAL=1;UNTIL=20380601T170000Z')
                    local_tz = dateutil.rrule.rrulestr(temp_rule, tzinfos=UTC_TIMEZONES, **{'forceset': True})._rrule[0]._dtstart.tzinfo

                    # Make a datetime object with tzinfo=<the DTSTART timezone>
                    # localized_until = datetime.datetime(2020, 6, 1, 17, 0, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
                    localized_until = make_aware(datetime.datetime.strptime(re.sub('^UNTIL=', '', naive_until), "%Y%m%dT%H%M%S"), local_tz)

                    # Coerce the datetime to UTC and format it as a string w/ Zulu format
                    # utc_until = UNTIL=20200601T220000Z
                    utc_until = 'UNTIL=' + localized_until.astimezone(pytz.utc).strftime('%Y%m%dT%H%M%SZ')

                    # rule was:    DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T170000
                    # rule is now: DTSTART;TZID=America/New_York:20200601T120000 RRULE:...;UNTIL=20200601T220000Z
                    rules[index] = rule.replace(naive_until, utc_until)
        return " ".join(rules)

    @classmethod
    def rrulestr(cls, rrule, ref_dt=None, **kwargs):
        """
        Apply our own custom rrule parsing requirements
        """
        rrule = Schedule.coerce_naive_until(rrule)
        kwargs['forceset'] = True
        rruleset = dateutil.rrule.rrulestr(rrule, tzinfos=UTC_TIMEZONES, **kwargs)

        _assert_timezone_id_is_valid(rruleset._rrule)
        _assert_timezone_id_is_valid(rruleset._exrule)

        # Fast forward is a way for us to limit the number of events in the rruleset
        # If we are fast forwarding and we don't have a count limited rule that is minutely or hourly
        # We will modify the start date of the rule to bring as close to the current date as possible
        # Even though the API restricts each rrule to have the same dtstart, each rrule in the rruleset
        # can fast forward to a difference dtstart. This is required in order to get stable occurrences.
        rruleset._rrule = _fast_forward_rrules(rruleset._rrule, ref_dt=ref_dt)
        rruleset._exrule = _fast_forward_rrules(rruleset._exrule, ref_dt=ref_dt)

        return rruleset

    def __str__(self):
        return u'%s_t%s_%s_%s' % (self.name, self.unified_job_template.id, self.id, self.next_run)

    def get_absolute_url(self, request=None):
        return reverse('api:schedule_detail', kwargs={'pk': self.pk}, request=request)

    def get_job_kwargs(self):
        config_data = self.prompts_dict()
        job_kwargs, rejected, errors = self.unified_job_template._accept_or_ignore_job_kwargs(**config_data)