Merge pull request #162 from grafana/matiasb-shift-rrule-until
Add UNTIL support for shifts recurrence rules
This commit is contained in:
commit
b0d98e772e
6 changed files with 80 additions and 7 deletions
|
|
@ -55,6 +55,7 @@ The above command returns JSON structured in the following way:
|
|||
| `duration` | No | Yes | Duration of the event. |
|
||||
| `frequency` | No | If type = `recurrent_event` or `rolling_users` | One of: `hourly`, `daily`, `weekly`, `monthly`. |
|
||||
| `interval` | No | Optional | This parameter takes a positive integer that represents the intervals that the recurrence rule repeats. |
|
||||
| `until` | No | Optional | When the recurrence rule ends (endless if None). This parameter takes a date format as `yyyy-MM-dd'T'HH:mm:ss` (for example "2020-09-05T08:00:00"). |
|
||||
| `week_start` | No | Optional | Start day of the week in iCal format. One of: `SU` (Sunday), `MO` (Monday), `TU` (Tuesday), `WE` (Wednesday), `TH` (Thursday), `FR` (Friday), `SA` (Saturday). Default: `SU`. |
|
||||
| `by_day` | No | Optional | List of days in iCal format. Valid values are: `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, `SA`. |
|
||||
| `by_month` | No | Optional | List of months. Valid values are `1` to `12`. |
|
||||
|
|
@ -72,7 +73,7 @@ Please see [RFC 5545](https://tools.ietf.org/html/rfc5545#section-3.3.10) for mo
|
|||
# Get OnCall shifts
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/SBM7DV7BKFUYU/" \
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
|
|
@ -159,7 +160,7 @@ The following available filter parameters should be provided as `GET` arguments:
|
|||
# Update OnCall shift
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/S3Z477AHDXTMF/" \
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \
|
||||
--request PUT \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
|
|
@ -198,7 +199,7 @@ The above command returns JSON structured in the following way:
|
|||
# Delete OnCall shift
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/S3Z477AHDXTMF/" \
|
||||
curl "{{API_URL}}/api/v1/on_call_shifts/OH3V5FYQEYJ6M/" \
|
||||
--request DELETE \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
"duration",
|
||||
"frequency",
|
||||
"interval",
|
||||
"until",
|
||||
"week_start",
|
||||
"by_day",
|
||||
"by_month",
|
||||
|
|
@ -214,12 +215,18 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
if type == CustomOnCallShift.TYPE_ROLLING_USERS_EVENT and index is None:
|
||||
raise BadRequest(detail="Field 'start_rotation_from_user_index' is required for this on-call shift type")
|
||||
|
||||
def _validate_start(self, start):
|
||||
def _validate_date_format(self, value):
|
||||
try:
|
||||
time.strptime(start, "%Y-%m-%dT%H:%M:%S")
|
||||
time.strptime(value, "%Y-%m-%dT%H:%M:%S")
|
||||
except (TypeError, ValueError):
|
||||
raise BadRequest(detail="Invalid datetime format, should be \"yyyy-mm-dd'T'hh:mm:ss\"")
|
||||
|
||||
def _validate_start(self, start):
|
||||
self._validate_date_format(start)
|
||||
|
||||
def _validate_until(self, until):
|
||||
self._validate_date_format(until)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if data.get("users", []) is None: # terraform case
|
||||
data["users"] = []
|
||||
|
|
@ -229,6 +236,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
data["source"] = CustomOnCallShift.SOURCE_API
|
||||
if data.get("start") is not None:
|
||||
self._validate_start(data["start"])
|
||||
if data.get("until") is not None:
|
||||
self._validate_until(data["until"])
|
||||
result = super().to_internal_value(data)
|
||||
return result
|
||||
|
||||
|
|
@ -236,6 +245,8 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
result = super().to_representation(instance)
|
||||
result["duration"] = int(instance.duration.total_seconds())
|
||||
result["start"] = instance.start.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
if instance.until is not None:
|
||||
result["until"] = instance.until.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
result = self._get_fields_to_represent(instance, result)
|
||||
return result
|
||||
|
||||
|
|
@ -245,6 +256,7 @@ class CustomOnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer
|
|||
CustomOnCallShift.TYPE_SINGLE_EVENT: [
|
||||
"frequency",
|
||||
"interval",
|
||||
"until",
|
||||
"by_day",
|
||||
"by_month",
|
||||
"by_monthday",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ invalid_field_data_7 = {
|
|||
"type": "invalid_type",
|
||||
}
|
||||
|
||||
invalid_field_data_8 = {
|
||||
"until": "not-a-date",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_on_call_shift(make_organization_and_user_with_token, make_on_call_shift, make_schedule):
|
||||
|
|
@ -80,17 +84,20 @@ def test_create_on_call_shift(make_organization_and_user_with_token):
|
|||
|
||||
url = reverse("api-public:on_call_shifts-list")
|
||||
|
||||
start = datetime.datetime.now()
|
||||
until = start + datetime.timedelta(days=30)
|
||||
data = {
|
||||
"team_id": None,
|
||||
"name": "test name",
|
||||
"type": "recurrent_event",
|
||||
"level": 1,
|
||||
"start": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"start": start.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"duration": 10800,
|
||||
"users": [user.public_primary_key],
|
||||
"week_start": "MO",
|
||||
"frequency": "weekly",
|
||||
"interval": 2,
|
||||
"until": until.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"by_day": ["MO", "WE", "FR"],
|
||||
}
|
||||
|
||||
|
|
@ -108,6 +115,7 @@ def test_create_on_call_shift(make_organization_and_user_with_token):
|
|||
"duration": data["duration"],
|
||||
"frequency": data["frequency"],
|
||||
"interval": data["interval"],
|
||||
"until": data["until"],
|
||||
"week_start": data["week_start"],
|
||||
"by_day": data["by_day"],
|
||||
"users": [user.public_primary_key],
|
||||
|
|
@ -163,6 +171,7 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal
|
|||
"duration": data_to_update["duration"],
|
||||
"frequency": "weekly",
|
||||
"interval": on_call_shift.interval,
|
||||
"until": None,
|
||||
"week_start": "SU",
|
||||
"by_day": data_to_update["by_day"],
|
||||
"users": [user.public_primary_key],
|
||||
|
|
@ -190,6 +199,7 @@ def test_update_on_call_shift(make_organization_and_user_with_token, make_on_cal
|
|||
invalid_field_data_5,
|
||||
invalid_field_data_6,
|
||||
invalid_field_data_7,
|
||||
invalid_field_data_8,
|
||||
],
|
||||
)
|
||||
def test_update_on_call_shift_invalid_field(make_organization_and_user_with_token, make_on_call_shift, data_to_update):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.5 on 2022-06-28 13:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('schedules', '0003_alter_customoncallshift_frequency'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='customoncallshift',
|
||||
name='until',
|
||||
field=models.DateTimeField(default=None, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -147,6 +147,8 @@ class CustomOnCallShift(models.Model):
|
|||
|
||||
interval = models.IntegerField(default=None, null=True) # every n days/months - ical format
|
||||
|
||||
until = models.DateTimeField(default=None, null=True) # if set, when recurrence ends
|
||||
|
||||
# week_start in ical format
|
||||
week_start = models.IntegerField(choices=WEEKDAY_CHOICES, default=SUNDAY) # for weekly events
|
||||
|
||||
|
|
@ -193,7 +195,7 @@ class CustomOnCallShift(models.Model):
|
|||
result += (
|
||||
f", frequency: {self.get_frequency_display()}, interval: {self.interval}, "
|
||||
f"week start: {self.week_start}, by day: {self.by_day}, by month: {self.by_month}, "
|
||||
f"by monthday: {self.by_monthday}"
|
||||
f"by monthday: {self.by_monthday}, until: {self.until.isoformat() if self.until else None}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
@ -297,6 +299,9 @@ class CustomOnCallShift(models.Model):
|
|||
rules["bymonthday"] = self.by_monthday
|
||||
if self.week_start is not None:
|
||||
rules["wkst"] = CustomOnCallShift.ICAL_WEEKDAY_MAP[self.week_start]
|
||||
if self.until is not None:
|
||||
time_zone = self.time_zone if self.time_zone is not None else "UTC"
|
||||
rules["until"] = self.convert_dt_to_schedule_timezone(self.until, time_zone)
|
||||
return rules
|
||||
|
||||
@cached_property
|
||||
|
|
|
|||
|
|
@ -199,3 +199,30 @@ def test_get_oncall_users_for_multiple_schedules(
|
|||
assert schedules.get_oncall_users(events_datetime=now + timezone.timedelta(minutes=30, seconds=1)) == [user_3]
|
||||
|
||||
assert schedules.get_oncall_users(events_datetime=now + timezone.timedelta(minutes=40, seconds=1)) == []
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_shift_convert_to_ical(make_organization_and_user, make_on_call_shift):
|
||||
organization, user = make_organization_and_user()
|
||||
|
||||
date = timezone.now().replace(tzinfo=None, microsecond=0)
|
||||
until = date + timezone.timedelta(days=30)
|
||||
|
||||
data = {
|
||||
"priority_level": 1,
|
||||
"start": date,
|
||||
"duration": timezone.timedelta(seconds=10800),
|
||||
"frequency": CustomOnCallShift.FREQUENCY_HOURLY,
|
||||
"interval": 1,
|
||||
"until": until,
|
||||
}
|
||||
|
||||
on_call_shift = make_on_call_shift(
|
||||
organization=organization, shift_type=CustomOnCallShift.TYPE_RECURRENT_EVENT, **data
|
||||
)
|
||||
on_call_shift.users.add(user)
|
||||
|
||||
ical_data = on_call_shift.convert_to_ical()
|
||||
ical_rrule_until = on_call_shift.until.strftime("%Y%m%dT%H%M%S")
|
||||
expected_rrule = f"RRULE:FREQ=HOURLY;UNTIL={ical_rrule_until}Z;INTERVAL=1;WKST=SU"
|
||||
assert expected_rrule in ical_data
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue