add support for datetime on final_shifts API parameters (#3103)

# What this PR does
- Changes the data type from `DateField` to `DateTimeField` on the
`final_shifts` API endpoint
- Accepts either a date or a datetime for the `start_date` and
`end_date` parameters (e.g. 2021-01-01 or 2021-01-01T01:00)
- Datetime granularity is in line with what is configurable on the
schedule i.e. `YYYY-MM-DD hh:mm`
- removes the rounding logic that modifies the query sent on the
database

## Which issue(s) this PR fixes
#3086 

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)

---------

Co-authored-by: Joey Orlando <joey.orlando@grafana.com>
This commit is contained in:
Nelson 2023-10-30 22:00:39 +10:00 committed by GitHub
parent d49ccef8cb
commit c281484f67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 40 additions and 18 deletions

View file

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Data type changed from `DateField` to `DateTimeField` on the `final_shifts` API endpoint. Endpoint now accepts either
a date or a datetime ([#3103](https://github.com/grafana/oncall/pull/3103))
### Changed ### Changed
- Simplify Direct Paging workflow. Now when using Direct Paging you either simply specify a team, or one or more users - Simplify Direct Paging workflow. Now when using Direct Paging you either simply specify a team, or one or more users

View file

@ -75,8 +75,8 @@ class ScheduleBaseSerializer(serializers.ModelSerializer):
class FinalShiftQueryParamsSerializer(serializers.Serializer): class FinalShiftQueryParamsSerializer(serializers.Serializer):
start_date = serializers.DateField(required=True) start_date = serializers.DateTimeField(required=True, input_formats=["%Y-%m-%dT%H:%M", "%Y-%m-%d"])
end_date = serializers.DateField(required=True) end_date = serializers.DateTimeField(required=True, input_formats=["%Y-%m-%dT%H:%M", "%Y-%m-%d"])
def validate(self, attrs): def validate(self, attrs):
if attrs["start_date"] > attrs["end_date"]: if attrs["start_date"] > attrs["end_date"]:

View file

@ -876,7 +876,7 @@ def test_oncall_shifts_request_validation(
organization, _, token = make_organization_and_user_with_token() organization, _, token = make_organization_and_user_with_token()
web_schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) web_schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
valid_date_msg = "Date has wrong format. Use one of these formats instead: YYYY-MM-DD." valid_date_msg = "Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm, YYYY-MM-DD."
client = APIClient() client = APIClient()
@ -917,6 +917,23 @@ def test_oncall_shifts_request_validation(
] ]
} }
# datetime validation
# invalid request (doesnt match pattern YYYY-MM-DDThh:mm)
response = _make_request(web_schedule, "?start_date=2021-01-01 01:00")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert (
response.json()["start_date"][0]
== "Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm, YYYY-MM-DD."
)
# valid request both parameters using datetime
response = _make_request(web_schedule, "?start_date=2021-01-01T01:00&end_date=2021-01-02T01:00")
assert response.status_code == status.HTTP_200_OK
# valid request combination of date and datetime
response = _make_request(web_schedule, "?start_date=2021-01-01&end_date=2021-01-02T01:00")
assert response.status_code == status.HTTP_200_OK
@pytest.mark.django_db @pytest.mark.django_db
def test_oncall_shifts_export( def test_oncall_shifts_export(
@ -958,7 +975,9 @@ def test_oncall_shifts_export(
client = APIClient() client = APIClient()
url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key})
response = client.get(f"{url}?start_date=2023-01-01&end_date=2023-02-01", format="json", HTTP_AUTHORIZATION=token) response = client.get(
f"{url}?start_date=2023-01-01T18:00&end_date=2023-02-01", format="json", HTTP_AUTHORIZATION=token
)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
expected_on_call_times = { expected_on_call_times = {
@ -1018,7 +1037,9 @@ def test_oncall_shifts_export_from_ical_schedule(
client = APIClient() client = APIClient()
url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key})
response = client.get(f"{url}?start_date=2023-07-01&end_date=2023-07-31", format="json", HTTP_AUTHORIZATION=token) response = client.get(
f"{url}?start_date=2023-07-01T09:00&end_date=2023-07-31T21:00", format="json", HTTP_AUTHORIZATION=token
)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
expected_on_call_times = { expected_on_call_times = {
@ -1055,7 +1076,9 @@ def test_oncall_shifts_export_from_api_schedule(
client = APIClient() client = APIClient()
url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key})
response = client.get(f"{url}?start_date=2023-07-01&end_date=2023-07-31", format="json", HTTP_AUTHORIZATION=token) response = client.get(
f"{url}?start_date=2023-07-01T09:00&end_date=2023-07-31T11:00", format="json", HTTP_AUTHORIZATION=token
)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
expected_on_call_times = { expected_on_call_times = {
@ -1098,7 +1121,9 @@ def test_oncall_shifts_export_truncate_events(
# request shifts on a Tu (ie. 00:00 - 09:00) # request shifts on a Tu (ie. 00:00 - 09:00)
url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key}) url = reverse("api-public:schedules-final-shifts", kwargs={"pk": schedule.public_primary_key})
response = client.get(f"{url}?start_date=2023-01-03&end_date=2023-01-03", format="json", HTTP_AUTHORIZATION=token) response = client.get(
f"{url}?start_date=2023-01-03&end_date=2023-01-03T09:00", format="json", HTTP_AUTHORIZATION=token
)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
expected_on_call_times = {user1_public_primary_key: 9} expected_on_call_times = {user1_public_primary_key: 9}

View file

@ -1,7 +1,5 @@
import datetime
import logging import logging
import pytz
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
@ -141,14 +139,8 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo
start_date = serializer.validated_data["start_date"] start_date = serializer.validated_data["start_date"]
end_date = serializer.validated_data["end_date"] end_date = serializer.validated_data["end_date"]
days_between_start_and_end = (end_date - start_date).days
datetime_start = datetime.datetime.combine(start_date, datetime.time.min, tzinfo=pytz.UTC) final_schedule_events: ScheduleEvents = schedule.final_events(start_date, end_date)
datetime_end = datetime_start + datetime.timedelta(
days=days_between_start_and_end, hours=23, minutes=59, seconds=59
)
final_schedule_events: ScheduleEvents = schedule.final_events(datetime_start, datetime_end)
logger.info( logger.info(
f"Exporting oncall shifts for schedule {pk} between dates {start_date} and {end_date}. {len(final_schedule_events)} shift events were found." f"Exporting oncall shifts for schedule {pk} between dates {start_date} and {end_date}. {len(final_schedule_events)} shift events were found."
) )
@ -159,8 +151,8 @@ class OnCallScheduleChannelView(RateLimitHeadersMixin, UpdateSerializerMixin, Mo
"user_email": user["email"], "user_email": user["email"],
"user_username": user["display_name"], "user_username": user["display_name"],
# truncate shift start/end exceeding the requested period # truncate shift start/end exceeding the requested period
"shift_start": event["start"] if event["start"] >= datetime_start else datetime_start, "shift_start": event["start"] if event["start"] >= start_date else start_date,
"shift_end": event["end"] if event["end"] <= datetime_end else datetime_end, "shift_end": event["end"] if event["end"] <= end_date else end_date,
} }
for event in final_schedule_events for event in final_schedule_events
for user in event["users"] for user in event["users"]