diff --git a/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js b/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js
index 272ed187dc..5b61eb54a6 100644
--- a/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js
+++ b/awx/ui/src/components/Schedule/shared/ScheduleFormFields.js
@@ -3,6 +3,7 @@ import { useField } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core';
import { t } from '@lingui/macro';
import styled from 'styled-components';
+import 'styled-components/macro';
import FormField from 'components/FormField';
import { required } from 'util/validators';
import { useConfig } from 'contexts/Config';
@@ -53,11 +54,11 @@ export default function ScheduleFormFields({
}
const config = useConfig();
- // const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
- // useField({
- // name: 'exceptionFrequency',
- // validate: required(t`Select a value for this field`),
- // });
+ const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
+ useField({
+ name: 'exceptionFrequency',
+ validate: required(t`Select a value for this field`),
+ });
const updateFrequency = (setFrequency) => (values) => {
setFrequency(values.sort(sortFrequencies));
@@ -151,34 +152,40 @@ export default function ScheduleFormFields({
/>
))}
- {/*
{t`Exceptions`}
-
- {t`Exceptions`}
+
+
- {t`None`}
- {t`Minute`}
- {t`Hour`}
- {t`Day`}
- {t`Week`}
- {t`Month`}
- {t`Year`}
-
-
+
+ {t`None`}
+ {t`Minute`}
+ {t`Hour`}
+ {t`Day`}
+ {t`Week`}
+ {t`Month`}
+ {t`Year`}
+
+
+
{exceptionFrequency.value.map((val) => (
- ))} */}
+ ))}
) : null}
>
diff --git a/awx/ui/src/components/Schedule/shared/buildRuleSet.js b/awx/ui/src/components/Schedule/shared/buildRuleSet.js
index c15594e0c6..0f8182bc01 100644
--- a/awx/ui/src/components/Schedule/shared/buildRuleSet.js
+++ b/awx/ui/src/components/Schedule/shared/buildRuleSet.js
@@ -39,7 +39,19 @@ export default function buildRuleSet(values) {
set.rrule(new RRule(rule));
});
- // TODO: exclusions
+ frequencies.forEach((frequency) => {
+ if (!values.exceptionFrequency?.includes(frequency)) {
+ return;
+ }
+ const rule = buildRuleObj({
+ startDate: values.startDate,
+ startTime: values.startTime,
+ timezone: values.timezone,
+ frequency,
+ ...values.exceptionOptions[frequency],
+ });
+ set.exrule(new RRule(rule));
+ });
return set;
}
diff --git a/awx/ui/src/components/Schedule/shared/buildRuleSet.test.js b/awx/ui/src/components/Schedule/shared/buildRuleSet.test.js
index 3d7831507c..42fde67981 100644
--- a/awx/ui/src/components/Schedule/shared/buildRuleSet.test.js
+++ b/awx/ui/src/components/Schedule/shared/buildRuleSet.test.js
@@ -243,4 +243,257 @@ RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`);
expect(ruleSet.toString()).toEqual(`DTSTART:20220613T123000Z
RRULE:INTERVAL=1;COUNT=1;FREQ=MINUTELY`);
});
+
+ test('should build minutely exception', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['minute'],
+ exceptionOptions: {
+ minute: {
+ interval: 3,
+ end: 'never',
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=MINUTELY',
+ ].join('\n')
+ );
+ });
+
+ test('should build hourly exception', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['hour'],
+ exceptionOptions: {
+ hour: {
+ interval: 3,
+ end: 'never',
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=HOURLY',
+ ].join('\n')
+ );
+ });
+
+ test('should build daily exception', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['day'],
+ exceptionOptions: {
+ day: {
+ interval: 3,
+ end: 'never',
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=DAILY',
+ ].join('\n')
+ );
+ });
+
+ test('should build weekly exception', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['week'],
+ exceptionOptions: {
+ week: {
+ interval: 3,
+ end: 'never',
+ daysOfWeek: [RRule.SU],
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=WEEKLY;BYDAY=SU',
+ ].join('\n')
+ );
+ });
+
+ test('should build monthly exception by day', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['month'],
+ exceptionOptions: {
+ month: {
+ interval: 3,
+ end: 'never',
+ runOn: 'day',
+ runOnDayNumber: 15,
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=MONTHLY;BYMONTHDAY=15',
+ ].join('\n')
+ );
+ });
+
+ test('should build monthly exception by weekday', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['month'],
+ exceptionOptions: {
+ month: {
+ interval: 3,
+ end: 'never',
+ runOn: 'the',
+ runOnTheOccurrence: 2,
+ runOnTheDay: 'monday',
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=3;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO',
+ ].join('\n')
+ );
+ });
+
+ test('should build annual exception by day', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['year'],
+ exceptionOptions: {
+ year: {
+ interval: 1,
+ end: 'never',
+ runOn: 'day',
+ runOnDayMonth: 3,
+ runOnDayNumber: 15,
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=1;FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15',
+ ].join('\n')
+ );
+ });
+
+ test('should build annual exception by weekday', () => {
+ const values = {
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ frequency: ['minute'],
+ frequencyOptions: {
+ minute: {
+ interval: 1,
+ end: 'never',
+ },
+ },
+ exceptionFrequency: ['year'],
+ exceptionOptions: {
+ year: {
+ interval: 1,
+ end: 'never',
+ runOn: 'the',
+ runOnTheOccurrence: 4,
+ runOnTheDay: 'monday',
+ runOnTheMonth: 6,
+ },
+ },
+ };
+
+ const ruleSet = buildRuleSet(values);
+ expect(ruleSet.toString()).toEqual(
+ [
+ 'DTSTART:20220613T123000Z',
+ 'RRULE:INTERVAL=1;FREQ=MINUTELY',
+ 'EXRULE:INTERVAL=1;FREQ=YEARLY;BYSETPOS=4;BYDAY=MO;BYMONTH=6',
+ ].join('\n')
+ );
+ });
});
diff --git a/awx/ui/src/components/Schedule/shared/parseRuleObj.js b/awx/ui/src/components/Schedule/shared/parseRuleObj.js
index 9699a6a51a..a676231193 100644
--- a/awx/ui/src/components/Schedule/shared/parseRuleObj.js
+++ b/awx/ui/src/components/Schedule/shared/parseRuleObj.js
@@ -32,6 +32,9 @@ export default function parseRuleObj(schedule) {
case 'RRULE':
values = parseRrule(ruleString, schedule, values);
break;
+ case 'EXRULE':
+ values = parseExRule(ruleString, schedule, values);
+ break;
default:
throw new UnsupportedRRuleError(`Unsupported rrule type: ${type}`);
}
@@ -79,6 +82,54 @@ const frequencyTypes = {
};
function parseRrule(rruleString, schedule, values) {
+ const { frequency, options } = parseRule(
+ rruleString,
+ schedule,
+ values.exceptionFrequency
+ );
+
+ if (values.frequencyOptions[frequency]) {
+ throw new UnsupportedRRuleError(
+ 'Duplicate exception frequency types not supported'
+ );
+ }
+
+ return {
+ ...values,
+ frequency: [...values.frequency, frequency].sort(sortFrequencies),
+ frequencyOptions: {
+ ...values.frequencyOptions,
+ [frequency]: options,
+ },
+ };
+}
+
+function parseExRule(exruleString, schedule, values) {
+ const { frequency, options } = parseRule(
+ exruleString,
+ schedule,
+ values.exceptionFrequency
+ );
+
+ if (values.exceptionOptions[frequency]) {
+ throw new UnsupportedRRuleError(
+ 'Duplicate exception frequency types not supported'
+ );
+ }
+
+ return {
+ ...values,
+ exceptionFrequency: [...values.exceptionFrequency, frequency].sort(
+ sortFrequencies
+ ),
+ exceptionOptions: {
+ ...values.exceptionOptions,
+ [frequency]: options,
+ },
+ };
+}
+
+function parseRule(ruleString, schedule, frequencies) {
const {
origOptions: {
bymonth,
@@ -90,7 +141,7 @@ function parseRrule(rruleString, schedule, values) {
interval,
until,
},
- } = RRule.fromString(rruleString);
+ } = RRule.fromString(ruleString);
const now = DateTime.now();
const closestQuarterHour = DateTime.fromMillis(
@@ -127,7 +178,7 @@ function parseRrule(rruleString, schedule, values) {
throw new Error(`Unexpected rrule frequency: ${freq}`);
}
const frequency = frequencyTypes[freq];
- if (values.frequency.includes(frequency)) {
+ if (frequencies.includes(frequency)) {
throw new Error(`Duplicate frequency types not supported (${frequency})`);
}
@@ -171,17 +222,9 @@ function parseRrule(rruleString, schedule, values) {
}
}
- if (values.frequencyOptions.frequency) {
- throw new UnsupportedRRuleError('Duplicate frequency types not supported');
- }
-
return {
- ...values,
- frequency: [...values.frequency, frequency].sort(sortFrequencies),
- frequencyOptions: {
- ...values.frequencyOptions,
- [frequency]: options,
- },
+ frequency,
+ options,
};
}
diff --git a/awx/ui/src/components/Schedule/shared/parseRuleObj.test.js b/awx/ui/src/components/Schedule/shared/parseRuleObj.test.js
index 9f0fcba1b6..426998bcc2 100644
--- a/awx/ui/src/components/Schedule/shared/parseRuleObj.test.js
+++ b/awx/ui/src/components/Schedule/shared/parseRuleObj.test.js
@@ -241,4 +241,51 @@ RRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=2;BYDAY=MO;UNTIL=20260602T170000Z`;
expect(parsed).toEqual(values);
});
+
+ test('should parse exemptions', () => {
+ const schedule = {
+ rrule: [
+ 'DTSTART;TZID=US/Eastern:20220608T123000',
+ 'RRULE:INTERVAL=1;FREQ=WEEKLY;BYDAY=MO',
+ 'EXRULE:INTERVAL=1;FREQ=MONTHLY;BYSETPOS=1;BYDAY=MO',
+ ].join(' '),
+ dtstart: '2022-06-13T16:30:00Z',
+ timezone: 'US/Eastern',
+ until: '',
+ dtend: null,
+ };
+
+ const parsed = parseRuleObj(schedule);
+
+ expect(parsed).toEqual({
+ startDate: '2022-06-13',
+ startTime: '12:30 PM',
+ timezone: 'US/Eastern',
+ frequency: ['week'],
+ frequencyOptions: {
+ week: {
+ interval: 1,
+ end: 'never',
+ occurrences: 1,
+ endDate: '2022-06-02',
+ endTime: '1:00 PM',
+ daysOfWeek: [RRule.MO],
+ },
+ },
+ exceptionFrequency: ['month'],
+ exceptionOptions: {
+ month: {
+ interval: 1,
+ end: 'never',
+ endDate: '2022-06-02',
+ endTime: '1:00 PM',
+ occurrences: 1,
+ runOn: 'the',
+ runOnDayNumber: 1,
+ runOnTheOccurrence: 1,
+ runOnTheDay: 'monday',
+ },
+ },
+ });
+ });
});