Merge pull request #288 from grafana/julia-web-schedule-backend-3

Create new oncall shift on update for started shifts
This commit is contained in:
Yulya Artyukhina 2022-07-28 17:30:35 +03:00 committed by GitHub
commit fdd369c16b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1286 additions and 31 deletions

View file

@ -30,13 +30,14 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
queryset=User.objects, required=False, allow_null=True
), # todo: filter by team?
)
updated_shift = serializers.CharField(read_only=True, allow_null=True, source="updated_shift.public_primary_key")
class Meta:
model = CustomOnCallShift
fields = [
"id",
"organization",
"name",
"title",
"type",
"schedule",
"priority_level",
@ -49,20 +50,20 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
"by_day",
"source",
"rolling_users",
"updated_shift",
]
extra_kwargs = {
"interval": {"required": False, "allow_null": True},
"source": {"required": False, "write_only": True},
}
SELECT_RELATED = ["schedule"]
SELECT_RELATED = ["schedule", "updated_shift"]
def get_shift_end(self, obj):
return obj.start + obj.duration
def to_internal_value(self, data):
data["source"] = CustomOnCallShift.SOURCE_WEB
data["week_start"] = CustomOnCallShift.MONDAY
if not data.get("shift_end"):
raise serializers.ValidationError({"shift_end": ["This field is required."]})
@ -73,19 +74,6 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
result = super().to_representation(instance)
return result
def validate_name(self, name):
organization = self.context["request"].auth.organization
if name is None:
return name
try:
obj = CustomOnCallShift.objects.get(organization=organization, name=name)
except CustomOnCallShift.DoesNotExist:
return name
if self.instance and obj.id == self.instance.id:
return name
else:
raise serializers.ValidationError(["On-call shift with this name already exists"])
def validate_by_day(self, by_day):
if by_day:
for day in by_day:
@ -105,7 +93,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
for users in rolling_users:
users_dict = dict()
for user in users:
users_dict[user.pk] = user.public_primary_key
users_dict[str(user.pk)] = user.public_primary_key
result.append(users_dict)
return result
@ -113,7 +101,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
if end <= start:
raise serializers.ValidationError({"shift_end": ["Incorrect shift end date"]})
def _validate_frequency(self, frequency, event_type, rolling_users, interval, by_day):
def _validate_frequency(self, frequency, event_type, rolling_users, interval, by_day, until):
if frequency is None:
if rolling_users and len(rolling_users) > 1:
raise serializers.ValidationError(
@ -123,6 +111,8 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
raise serializers.ValidationError({"interval": ["Cannot set interval for non-recurrent shifts"]})
if by_day:
raise serializers.ValidationError({"by_day": ["Cannot set days value for non-recurrent shifts"]})
if until:
raise serializers.ValidationError({"until": ["Cannot set 'until' for non-recurrent shifts"]})
else:
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
raise serializers.ValidationError(
@ -142,10 +132,6 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
def _correct_validated_data(self, event_type, validated_data):
fields_to_update_for_overrides = [
"priority_level",
"frequency",
"interval",
"by_day",
"until",
"rotation_start",
]
if event_type == CustomOnCallShift.TYPE_OVERRIDE:
@ -163,6 +149,7 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
validated_data.get("rolling_users"),
validated_data.get("interval"),
validated_data.get("by_day"),
validated_data.get("until"),
)
self._validate_rotation_start(validated_data["start"], validated_data["rotation_start"])
self._validate_until(validated_data["rotation_start"], validated_data.get("until"))
@ -176,11 +163,15 @@ class OnCallShiftSerializer(EagerLoadingMixin, serializers.ModelSerializer):
if validated_data.get("schedule"):
validated_data["team"] = validated_data["schedule"].team
validated_data["week_start"] = CustomOnCallShift.MONDAY
return validated_data
def create(self, validated_data):
validated_data = self._correct_validated_data(validated_data["type"], validated_data)
validated_data["name"] = CustomOnCallShift.generate_name(
validated_data["schedule"], validated_data["priority_level"], validated_data["type"]
)
instance = super().create(validated_data)
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
@ -196,8 +187,26 @@ class OnCallShiftUpdateSerializer(OnCallShiftSerializer):
def update(self, instance, validated_data):
validated_data = self._correct_validated_data(instance.type, validated_data)
change_only_title = True
create_or_update_last_shift = False
result = super().update(instance, validated_data)
for field in validated_data:
if field != "title" and validated_data[field] != getattr(instance, field):
change_only_title = False
break
if not change_only_title:
if instance.type != CustomOnCallShift.TYPE_OVERRIDE:
if instance.event_is_started:
create_or_update_last_shift = True
elif instance.event_is_finished:
raise serializers.ValidationError(["This event cannot be updated"])
if create_or_update_last_shift:
result = instance.create_or_update_last_shift(validated_data)
else:
result = super().update(instance, validated_data)
instance.start_drop_ical_and_check_schedule_tasks(instance.schedule)
return result

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2022-07-22 11:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('schedules', '0006_customoncallshift_rotation_start'),
]
operations = [
migrations.AddField(
model_name='customoncallshift',
name='updated_shift',
field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_shift', to='schedules.customoncallshift'),
),
migrations.AddField(
model_name='customoncallshift',
name='title',
field=models.CharField(default=None, max_length=200, null=True),
),
]

View file

@ -1,4 +1,6 @@
import logging
import random
import string
from calendar import monthrange
from uuid import uuid4
@ -6,8 +8,9 @@ import pytz
from django.apps import apps
from django.conf import settings
from django.core.validators import MinLengthValidator
from django.db import models
from django.db import models, transaction
from django.db.models import JSONField
from django.forms.models import model_to_dict
from django.utils import timezone
from django.utils.functional import cached_property
from icalendar.cal import Event
@ -165,6 +168,7 @@ class CustomOnCallShift(models.Model):
default=None,
)
name = models.CharField(max_length=200)
title = models.CharField(max_length=200, null=True, default=None)
time_zone = models.CharField(max_length=100, null=True, default=None)
source = models.IntegerField(choices=SOURCE_CHOICES, default=SOURCE_API)
users = models.ManyToManyField("user_management.User") # users in single and recurrent events
@ -196,16 +200,44 @@ class CustomOnCallShift(models.Model):
by_month = JSONField(default=None, null=True) # [] BYMONTH - what months (1, 2, 3, ...) - ical format
by_monthday = JSONField(default=None, null=True) # [] BYMONTHDAY - what days of month (1, 2, -3) - ical format
updated_shift = models.OneToOneField(
"schedules.CustomOnCallShift",
on_delete=models.SET_NULL,
default=None,
null=True,
related_name="parent_shift",
)
class Meta:
unique_together = ("name", "organization")
def delete(self, *args, **kwargs):
for schedule in self.schedules.all():
self.start_drop_ical_and_check_schedule_tasks(schedule)
schedules_to_update = list(self.schedules.all())
if self.schedule:
self.start_drop_ical_and_check_schedule_tasks(self.schedule)
# todo: add soft delete
super().delete(*args, **kwargs)
schedules_to_update.append(self.schedule)
# do soft delete for started shifts that were created for web schedule
if self.schedule and self.event_is_started:
self.until = timezone.now().replace(microsecond=0)
self.save(update_fields=["until"])
else:
super().delete(*args, **kwargs)
for schedule in schedules_to_update:
self.start_drop_ical_and_check_schedule_tasks(schedule)
@property
def event_is_started(self):
return bool(self.rotation_start <= timezone.now())
@property
def event_is_finished(self):
if self.frequency is not None:
is_finished = bool(self.until <= timezone.now()) if self.until else False
else:
is_finished = bool(self.start + self.duration <= timezone.now())
return is_finished
@property
def repr_settings_for_client_side_logging(self) -> str:
@ -328,7 +360,7 @@ class CustomOnCallShift(models.Model):
if event.start.date() >= next_event_start.date():
next_event = event
break
next_event_dt = next_event.start
next_event_dt = next_event.start if next_event is not None else None
return next_event_dt
@cached_property
@ -399,3 +431,51 @@ class CustomOnCallShift(models.Model):
drop_cached_ical_task.apply_async((schedule.pk,))
schedule_notify_about_empty_shifts_in_schedule.apply_async((schedule.pk,))
schedule_notify_about_gaps_in_schedule.apply_async((schedule.pk,))
@cached_property
def last_updated_shift(self):
last_shift = self.updated_shift
if last_shift is not None:
while last_shift.updated_shift is not None:
last_shift = last_shift.updated_shift
return last_shift
def create_or_update_last_shift(self, data):
# rotation start date cannot be earlier than now
data["rotation_start"] = max(data["rotation_start"], timezone.now().replace(microsecond=0))
# prepare dict with params of existing instance with last updates and remove unique and m2m fields from it
shift_to_update = self.last_updated_shift or self
instance_data = model_to_dict(shift_to_update)
fields_to_remove = ["id", "public_primary_key", "uuid", "users", "updated_shift", "name"]
for field in fields_to_remove:
instance_data.pop(field)
instance_data.update(data)
instance_data["schedule"] = self.schedule
instance_data["team"] = self.team
if self.last_updated_shift is None or self.last_updated_shift.event_is_started:
# create new shift
instance_data["name"] = CustomOnCallShift.generate_name(
self.schedule, instance_data["priority_level"], instance_data["type"]
)
with transaction.atomic():
shift = CustomOnCallShift(**instance_data)
shift.save()
shift_to_update.until = data["rotation_start"]
shift_to_update.updated_shift = shift
shift_to_update.save(update_fields=["until", "updated_shift"])
else:
shift = self.last_updated_shift
for key in instance_data:
setattr(shift, key, instance_data[key])
shift.save(update_fields=list(instance_data))
return shift
@staticmethod
def generate_name(schedule, priority_level, shift_type):
shift_type_name = "override" if shift_type == CustomOnCallShift.TYPE_OVERRIDE else "rotation"
name = f"{schedule.name}-{shift_type_name}-{priority_level}-"
name += "".join(random.choice(string.ascii_lowercase) for _ in range(5))
return name