Skip to content
Snippets Groups Projects
Verified Commit 2b5537b3 authored by Ernesto Rico Schmidt's avatar Ernesto Rico Schmidt
Browse files

Add type annotations

parent 7b30f5e7
No related branches found
No related tags found
No related merge requests found
Pipeline #3102 passed
...@@ -41,6 +41,7 @@ class ScheduleData(TypedDict): ...@@ -41,6 +41,7 @@ class ScheduleData(TypedDict):
add_days_no: int | None add_days_no: int | None
by_weekday: int | None by_weekday: int | None
default_playlist_id: int | None default_playlist_id: int | None
dryrun: bool | None
end_time: str end_time: str
first_date: str first_date: str
id: int | None id: int | None
...@@ -79,8 +80,14 @@ class Conflicts(TypedDict): ...@@ -79,8 +80,14 @@ class Conflicts(TypedDict):
solutions: dict[str, str] solutions: dict[str, str]
# TODO: add type annotations class ScheduleCreateUpdateData(TypedDict):
def resolve_conflicts(data, schedule_pk, show_pk): notes: dict
playlists: dict
schedule: ScheduleData
solutions: dict[str, str]
def resolve_conflicts(data: ScheduleCreateUpdateData, schedule_pk: int | None, show_pk: int):
""" """
Resolves conflicts Resolves conflicts
Expects JSON POST/PUT data from /shows/1/schedules/ Expects JSON POST/PUT data from /shows/1/schedules/
...@@ -89,21 +96,20 @@ def resolve_conflicts(data, schedule_pk, show_pk): ...@@ -89,21 +96,20 @@ def resolve_conflicts(data, schedule_pk, show_pk):
Returns an empty list if resolution was successful Returns an empty list if resolution was successful
""" """
sdl = data["schedule"] schedule = data["schedule"]
solutions = data.get("solutions", []) solutions = data.get("solutions", []) # only needed if conflicts exist
# Regenerate conflicts new_schedule = instantiate_upcoming_schedule(schedule, show_pk, schedule_pk)
schedule = instantiate_upcoming_schedule(sdl, show_pk, schedule_pk) show = new_schedule.show
show = schedule.show conflicts = make_conflicts(schedule, schedule_pk, show_pk)
conflicts = make_conflicts(sdl, schedule_pk, show_pk)
if schedule.rrule.freq > 0 and schedule.first_date == schedule.last_date: if new_schedule.rrule.freq > 0 and new_schedule.first_date == new_schedule.last_date:
raise ValidationError( raise ValidationError(
_("Start and end dates must not be the same."), _("Start and end dates must not be the same."),
code="no-same-day-start-and-end", code="no-same-day-start-and-end",
) )
if schedule.last_date < schedule.first_date: if new_schedule.last_date < new_schedule.first_date:
raise ValidationError( raise ValidationError(
_("End date mustn't be before start."), _("End date mustn't be before start."),
code="no-start-after-end", code="no-start-after-end",
...@@ -118,177 +124,159 @@ def resolve_conflicts(data, schedule_pk, show_pk): ...@@ -118,177 +124,159 @@ def resolve_conflicts(data, schedule_pk, show_pk):
conflicts=conflicts, conflicts=conflicts,
) )
# Projected timeslots to create to_create: list[TimeSlot] = []
create = [] to_update: list[TimeSlot] = []
to_delete: list[TimeSlot] = []
# Existing timeslots to update
update = []
# Existing timeslots to delete
delete = []
# Error messages
errors = {} errors = {}
for ts in conflicts["projected"]: for timeslot in conflicts["projected"]:
# If no solution necessary # If no solution necessary: Create the projected timeslot and skip
# if "solution_choices" not in timeslot or len(timeslot["collisions"]) == 0:
# - Create the projected timeslot and skip to_create.append(
# TimeSlot.objects.instantiate(
if "solution_choices" not in ts or len(ts["collisions"]) < 1: timeslot["start"], timeslot["end"], new_schedule, show
projected_ts = TimeSlot.objects.instantiate(ts["start"], ts["end"], schedule, show) ),
create.append(projected_ts) )
continue continue
# Check hash (if start, end, rrule or by_weekday changed) # Check hash (if start, end, rrule or by_weekday changed)
if not ts["hash"] in solutions: if not timeslot["hash"] in solutions:
errors[ts["hash"]] = _("This change on the timeslot is not allowed.") errors[timeslot["hash"]] = _("This change on the timeslot is not allowed.")
continue continue
# If no resolution given # If no resolution given: skip
# if solutions[timeslot["hash"]] == "":
# - Skip errors[timeslot["hash"]] = _("No solution given.")
#
if solutions[ts["hash"]] == "":
errors[ts["hash"]] = _("No solution given.")
continue continue
# If resolution is not accepted for this conflict # If resolution is not accepted for this conflict: SKIP
# if not solutions[timeslot["hash"]] in timeslot["solution_choices"]:
# - Skip errors[timeslot["hash"]] = _("Given solution is not accepted for this conflict.")
#
if not solutions[ts["hash"]] in ts["solution_choices"]:
errors[ts["hash"]] = _("Given solution is not accepted for this conflict.")
continue continue
"""Conflict resolution""" """Conflict resolution"""
existing = ts["collisions"][0] existing = timeslot["collisions"][0]
solution = solutions[ts["hash"]] solution = solutions[timeslot["hash"]]
# theirs
#
# - Discard the projected timeslot
# - Keep the existing collision(s)
#
if solution == "theirs": if solution == "theirs":
# - Discard the projected timeslot
# - Keep the existing collision(s)
continue continue
# ours
#
# - Create the projected timeslot
# - Delete the existing collision(s)
#
if solution == "ours": if solution == "ours":
projected_ts = TimeSlot.objects.instantiate(ts["start"], ts["end"], schedule, show) # - Create the projected timeslot
create.append(projected_ts) # - Delete the existing collision(s)
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], timeslot["end"], new_schedule, show
),
)
# Delete collision(s) # Delete collision(s)
for ex in ts["collisions"]: for collision in timeslot["collisions"]:
try: try:
existing_ts = TimeSlot.objects.get(pk=ex["id"]) to_delete.append(TimeSlot.objects.get(pk=collision["id"]))
delete.append(existing_ts)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
# theirs-end
#
# - Keep the existing timeslot
# - Create projected with end of existing start
#
if solution == "theirs-end": if solution == "theirs-end":
projected_ts = TimeSlot.objects.instantiate( # - Keep the existing timeslot
ts["start"], existing["start"], schedule, show # - Create projected with end of existing start
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], existing["start"], new_schedule, show
),
) )
create.append(projected_ts)
# ours-end
#
# - Create the projected timeslot
# - Change the start of the existing collision to projected end
#
if solution == "ours-end": if solution == "ours-end":
projected_ts = TimeSlot.objects.instantiate(ts["start"], ts["end"], schedule, show) # - Create the projected timeslot
create.append(projected_ts) # - Change the start of the existing collision to projected end
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], timeslot["end"], new_schedule, show
),
)
existing_ts = TimeSlot.objects.get(pk=existing["id"]) existing_ts = TimeSlot.objects.get(pk=existing["id"])
existing_ts.start = parse_datetime(ts["end"]) existing_ts.start = parse_datetime(timeslot["end"])
update.append(existing_ts) to_update.append(existing_ts)
# theirs-start
#
# - Keep existing
# - Create projected with start time of existing end
#
if solution == "theirs-start": if solution == "theirs-start":
projected_ts = TimeSlot.objects.instantiate(existing["end"], ts["end"], schedule, show) # - Keep existing
create.append(projected_ts) # - Create projected with start time of existing end
to_create.append(
# ours-start TimeSlot.objects.instantiate(existing["end"], timeslot["end"], new_schedule, show),
# )
# - Create the projected timeslot
# - Change end of existing to projected start
#
if solution == "ours-start": if solution == "ours-start":
projected_ts = TimeSlot.objects.instantiate(ts["start"], ts["end"], schedule, show) # - Create the projected timeslot
create.append(projected_ts) # - Change end of existing to projected start
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], timeslot["end"], new_schedule, show
),
)
existing_ts = TimeSlot.objects.get(pk=existing["id"]) existing_ts = TimeSlot.objects.get(pk=existing["id"])
existing_ts.end = parse_datetime(ts["start"]) existing_ts.end = parse_datetime(timeslot["start"])
update.append(existing_ts) to_update.append(existing_ts)
# theirs-both
#
# - Keep existing
# - Create two projected timeslots with end of existing start and start of existing
# end
#
if solution == "theirs-both": if solution == "theirs-both":
projected_ts = TimeSlot.objects.instantiate( # - Keep existing
ts["start"], existing["start"], schedule, show # - Create two projected timeslots with end of existing start and start of existing end
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], existing["start"], new_schedule, show
),
) )
create.append(projected_ts)
to_create.append(
projected_ts = TimeSlot.objects.instantiate(existing["end"], ts["end"], schedule, show) TimeSlot.objects.instantiate(existing["end"], timeslot["end"], new_schedule, show),
create.append(projected_ts) )
# ours-both
#
# - Create projected
# - Split existing into two:
# - Set existing end time to projected start
# - Create another one with start = projected end and end = existing end
#
if solution == "ours-both": if solution == "ours-both":
projected_ts = TimeSlot.objects.instantiate(ts["start"], ts["end"], schedule, show) # - Create projected
create.append(projected_ts) # - Split existing into two
# - Set existing end time to projected start
# - Create another one with start = projected end and end = existing end
to_create.append(
TimeSlot.objects.instantiate(
timeslot["start"], timeslot["end"], new_schedule, show
),
)
existing_ts = TimeSlot.objects.get(pk=existing["id"]) existing_ts = TimeSlot.objects.get(pk=existing["id"])
existing_ts.end = parse_datetime(ts["start"]) existing_ts.end = parse_datetime(timeslot["start"])
update.append(existing_ts) to_update.append(existing_ts)
projected_ts = TimeSlot.objects.instantiate(ts["end"], existing["end"], schedule, show) to_create.append(
create.append(projected_ts) TimeSlot.objects.instantiate(timeslot["end"], existing["end"], new_schedule, show),
)
# If there were any errors, don't make any db changes yet # If there were any errors, don't make any db changes yet
# but add error messages and return already chosen solutions # but add error messages and return already chosen solutions
if len(errors) > 0: if len(errors) > 0:
conflicts = make_conflicts(model_to_dict(schedule), schedule.pk, show.pk) conflicts = make_conflicts(model_to_dict(new_schedule), new_schedule.pk, show.pk)
partly_resolved = conflicts["projected"] partly_resolved = conflicts["projected"]
saved_solutions = {} saved_solutions = {}
# Add already chosen resolutions and error message to conflict # Add already chosen resolutions and error message to conflict
for index, c in enumerate(conflicts["projected"]): for index, projected_entry in enumerate(conflicts["projected"]):
# The element should only exist if there was a collision # The element should only exist if there was a collision
if len(c["collisions"]) > 0: if len(projected_entry["collisions"]) > 0:
saved_solutions[c["hash"]] = "" saved_solutions[projected_entry["hash"]] = ""
if c["hash"] in solutions and solutions[c["hash"]] in c["solution_choices"]: if (
saved_solutions[c["hash"]] = solutions[c["hash"]] projected_entry["hash"] in solutions
and solutions[projected_entry["hash"]] in projected_entry["solution_choices"]
):
saved_solutions[projected_entry["hash"]] = solutions[projected_entry["hash"]]
if c["hash"] in errors: if projected_entry["hash"] in errors:
partly_resolved[index]["error"] = errors[c["hash"]] partly_resolved[index]["error"] = errors[projected_entry["hash"]]
# Re-insert post data # Re-insert post data
conflicts["projected"] = partly_resolved conflicts["projected"] = partly_resolved
...@@ -302,61 +290,55 @@ def resolve_conflicts(data, schedule_pk, show_pk): ...@@ -302,61 +290,55 @@ def resolve_conflicts(data, schedule_pk, show_pk):
conflicts=conflicts, conflicts=conflicts,
) )
# Collect upcoming timeslots to delete which might still remain remaining_timeslots = TimeSlot.objects.filter(
del_timeslots = TimeSlot.objects.filter( schedule=new_schedule,
schedule=schedule, start__gt=timezone.make_aware(datetime.combine(new_schedule.last_date, time(0, 0))),
start__gt=timezone.make_aware(datetime.combine(schedule.last_date, time(0, 0))),
) )
for del_ts in del_timeslots: for timeslot in remaining_timeslots:
delete.append(del_ts) to_delete.append(timeslot)
# If 'dryrun' is true, just return the projected changes instead of executing them # If 'dryrun' is true, just return the projected changes instead of executing them
if "dryrun" in sdl and sdl["dryrun"]: if "dryrun" in schedule and schedule["dryrun"]:
return { return {
"create": [model_to_dict(ts) for ts in create], "create": [model_to_dict(timeslot) for timeslot in to_create],
"update": [model_to_dict(ts) for ts in update], "update": [model_to_dict(timeslot) for timeslot in to_update],
"delete": [model_to_dict(ts) for ts in delete], "delete": [model_to_dict(timeslot) for timeslot in to_delete],
} }
"""Database changes if no errors found""" # Database changes if no errors found
# Only save schedule if timeslots were created if to_create:
if create: new_schedule.save()
# Create or update schedule
schedule.save()
# Update timeslots for timeslot in to_update:
for ts in update: timeslot.save(update_fields=["start", "end"])
ts.save(update_fields=["start", "end"])
# Create timeslots for timeslot in to_create:
for ts in create: timeslot.schedule = new_schedule
ts.schedule = schedule
# Reassign playlists # Reassign playlists
if "playlists" in data and ts.hash in data["playlists"]: if "playlists" in data and timeslot.hash in data["playlists"]:
ts.playlist_id = int(data["playlists"][ts.hash]) timeslot.playlist_id = int(data["playlists"][timeslot.hash])
ts.save() timeslot.save()
# Reassign notes # Reassign notes
if "notes" in data and ts.hash in data["notes"]: if "notes" in data and timeslot.hash in data["notes"]:
try: try:
note = Note.objects.get(pk=int(data["notes"][ts.hash])) note = Note.objects.get(pk=int(data["notes"][timeslot.hash]))
note.timeslot_id = ts.id note.timeslot_id = timeslot.id
note.save(update_fields=["timeslot_id"]) note.save(update_fields=["timeslot_id"])
timeslot = TimeSlot.objects.get(pk=ts.id) timeslot = TimeSlot.objects.get(pk=timeslot.id)
timeslot.note_id = note.id timeslot.note_id = note.id
timeslot.save(update_fields=["note_id"]) timeslot.save(update_fields=["note_id"])
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
# Delete manually resolved timeslots and those after until for timeslot in to_delete:
for dl in delete: timeslot.delete()
dl.delete()
return model_to_dict(schedule) return model_to_dict(new_schedule)
def instantiate_upcoming_schedule( def instantiate_upcoming_schedule(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment