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

Code before:

        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)

    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)
        if errors:
            logger.info('Errors creating scheduled job: {}'.format(errors))
        job_kwargs['_eager_fields'] = {'launch_type': 'scheduled', 'schedule': self}

Review comment (bug):
Since there's nothing wrong with the runtime, this would also need to align with the above suggestions to represent the case with a different exception:
```suggestion
                            except ValueError as val_err:
```

Code after:
                    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)
        if errors:
            logger.info('Errors creating scheduled job: {}'.format(errors))
        job_kwargs['_eager_fields'] = {'launch_type': 'scheduled', 'schedule': self}
        return job_kwargs

    def get_end_date(ruleset):
        # if we have a complex ruleset with a lot of options getting the last index of the ruleset can take some time
        # And a ruleset without a count/until can come back as datetime.datetime(9999, 12, 31, 15, 0, tzinfo=tzfile('US/Eastern'))
        # So we are going to do a quick scan to make sure we would have an end date