Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
steering
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Container Registry
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
AURA
steering
Commits
759e2bac
Verified
Commit
759e2bac
authored
9 months ago
by
Ernesto Rico Schmidt
Browse files
Options
Downloads
Patches
Plain Diff
feat: generate virtual timeslots for unscheduled calendar areas
parent
aa8b5b10
No related branches found
No related tags found
No related merge requests found
Pipeline
#8218
passed
9 months ago
Stage: check
Stage: test
Stage: build
Stage: deploy
Stage: release
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
program/services.py
+130
-12
130 additions, 12 deletions
program/services.py
program/views.py
+12
-87
12 additions, 87 deletions
program/views.py
with
142 additions
and
99 deletions
program/services.py
+
130
−
12
View file @
759e2bac
...
...
@@ -19,7 +19,8 @@
import
copy
from
datetime
import
datetime
,
time
,
timedelta
from
typing
import
TypedDict
from
itertools
import
pairwise
from
typing
import
Literal
,
TypedDict
from
dateutil.relativedelta
import
relativedelta
from
dateutil.rrule
import
rrule
...
...
@@ -30,7 +31,15 @@ from django.core.exceptions import ObjectDoesNotExist
from
django.db.models
import
Q
,
QuerySet
from
django.utils
import
timezone
from
django.utils.translation
import
gettext_lazy
as
_
from
program.models
import
Note
,
RRule
,
Schedule
,
ScheduleConflictError
,
Show
,
TimeSlot
from
program.models
import
(
Note
,
RadioSettings
,
RRule
,
Schedule
,
ScheduleConflictError
,
Show
,
TimeSlot
,
)
from
program.serializers
import
ScheduleSerializer
,
TimeSlotSerializer
from
program.utils
import
parse_date
,
parse_datetime
,
parse_time
...
...
@@ -86,6 +95,36 @@ class ScheduleCreateUpdateData(TypedDict):
solutions
:
dict
[
str
,
str
]
class
ScheduleEntry
(
TypedDict
):
end
:
str
is_virtual
:
bool
show_id
:
int
start
:
str
title
:
str
class
TimeslotEntry
(
TypedDict
):
end
:
str
id
:
int
is_virtual
:
Literal
[
False
]
playlist_id
:
int
|
None
repetition_of_id
:
int
|
None
schedule_default_playlist_id
:
int
|
None
schedule_id
:
int
show_default_playlist_id
:
int
|
None
show_id
:
int
start
:
str
title
:
str
class
VirtualTimeslotEntry
(
TypedDict
):
end
:
str
is_virtual
:
Literal
[
True
]
show_id
:
int
start
:
str
title
:
str
def
create_timeslot
(
start
:
str
,
end
:
str
,
schedule
:
Schedule
)
->
TimeSlot
:
"""
Creates and returns an unsaved timeslot with the given `start`, `end` and `schedule`.
"""
...
...
@@ -740,14 +779,93 @@ def generate_conflicts(timeslots: list[TimeSlot]) -> Conflicts:
return
conflicts
def
get_timerange_timeslots
(
start
:
datetime
,
end
:
datetime
)
->
QuerySet
[
TimeSlot
]:
"""
Gets a queryset of timeslots between the given `start` and `end` datetime.
"""
def
make_schedule_entry
(
*
,
timeslot_entry
:
TimeslotEntry
)
->
ScheduleEntry
:
"""
returns a schedule entry for the given timeslot entry.
"""
return
{
"
end
"
:
timeslot_entry
[
"
end
"
],
"
show_id
"
:
timeslot_entry
[
"
show_id
"
],
"
is_virtual
"
:
timeslot_entry
[
"
is_virtual
"
],
"
start
"
:
timeslot_entry
[
"
start
"
],
"
title
"
:
timeslot_entry
[
"
title
"
],
}
def
make_timeslot_entry
(
*
,
timeslot
:
TimeSlot
)
->
TimeslotEntry
:
"""
returns a timeslot entry for the given timeslot.
"""
schedule
=
timeslot
.
schedule
show
=
timeslot
.
schedule
.
show
return
{
"
end
"
:
timeslot
.
end
.
strftime
(
"
%Y-%m-%dT%H:%M:%S %z
"
),
"
id
"
:
timeslot
.
id
,
"
is_virtual
"
:
False
,
"
playlist_id
"
:
timeslot
.
playlist_id
,
# 'timeslot.repetition_of` is a foreign key that can be null
"
repetition_of_id
"
:
timeslot
.
repetition_of
.
id
if
timeslot
.
repetition_of
else
None
,
"
schedule_default_playlist_id
"
:
schedule
.
default_playlist_id
,
"
schedule_id
"
:
schedule
.
id
,
"
show_default_playlist_id
"
:
show
.
default_playlist_id
,
"
show_id
"
:
show
.
id
,
"
start
"
:
timeslot
.
start
.
strftime
(
"
%Y-%m-%dT%H:%M:%S %z
"
),
"
title
"
:
f
"
{
show
.
name
}
{
_
(
'
REP
'
)
}
"
if
schedule
.
is_repetition
else
show
.
name
,
}
def
make_virtual_timeslot_entry
(
*
,
gap_start
:
datetime
,
gap_end
:
datetime
)
->
VirtualTimeslotEntry
:
"""
returns a virtual timeslot entry to fill the gap in between `gap_start` and `gap_end`.
"""
return
{
"
end
"
:
gap_end
.
strftime
(
"
%Y-%m-%dT%H:%M:%S %z
"
),
"
is_virtual
"
:
True
,
"
show_id
"
:
RadioSettings
.
objects
.
first
().
fallback_show
.
id
,
"
start
"
:
gap_start
.
strftime
(
"
%Y-%m-%dT%H:%M:%S %z
"
),
"
title
"
:
RadioSettings
.
objects
.
first
().
fallback_default_pool
,
}
def
get_timerange_timeslot_entries
(
timerange_start
:
datetime
,
timerange_end
:
datetime
,
include_virtual
:
bool
=
False
)
->
list
[
TimeslotEntry
|
VirtualTimeslotEntry
]:
"""
Gets list of timeslot entries between the given `timerange_start` and `timerange_end`.
Include virtual timeslots if requested.
"""
timeslots
=
TimeSlot
.
objects
.
filter
(
# start before `timerange_start` and end after `timerange_start`
Q
(
start__lt
=
timerange_start
,
end__gt
=
timerange_start
)
# start after/at `timerange_start`, end before/at `timerange_end`
|
Q
(
start__gte
=
timerange_start
,
end__lte
=
timerange_end
)
# start before `timerange_end`, end after/at `timerange_end`
|
Q
(
start__lt
=
timerange_end
,
end__gte
=
timerange_end
)
).
select_related
(
"
schedule
"
)
if
not
include_virtual
:
return
[
make_timeslot_entry
(
timeslot
=
timeslot
)
for
timeslot
in
timeslots
]
timeslot_entries
=
[]
# gap before the first timeslot
first_timeslot
=
timeslots
.
first
()
if
first_timeslot
.
start
>
timerange_start
:
timeslot_entries
.
append
(
make_virtual_timeslot_entry
(
gap_start
=
timerange_start
,
gap_end
=
first_timeslot
.
start
)
)
return
TimeSlot
.
objects
.
filter
(
# start before `start` and end after `start`
Q
(
start__lt
=
start
,
end__gt
=
start
)
# start after/at `start`, end before/at `end`
|
Q
(
start__gte
=
start
,
end__lte
=
end
)
# start before `end`, end after/at `end`
|
Q
(
start__lt
=
end
,
end__gte
=
end
)
)
for
index
,
(
current
,
upcoming
)
in
enumerate
(
pairwise
(
timeslots
)):
timeslot_entries
.
append
(
make_timeslot_entry
(
timeslot
=
current
))
# gap between the timeslots
if
current
.
end
!=
upcoming
.
start
:
timeslot_entries
.
append
(
make_virtual_timeslot_entry
(
gap_start
=
current
.
end
,
gap_end
=
upcoming
.
start
)
)
# gap after the last timeslot
last_timeslot
=
timeslots
.
last
()
if
last_timeslot
.
end
<
timerange_end
:
timeslot_entries
.
append
(
make_virtual_timeslot_entry
(
gap_start
=
last_timeslot
.
end
,
gap_end
=
timerange_end
)
)
return
timeslot_entries
This diff is collapsed.
Click to expand it.
program/views.py
+
12
−
87
View file @
759e2bac
...
...
@@ -20,7 +20,6 @@
import
logging
from
datetime
import
date
,
datetime
,
time
,
timedelta
from
itertools
import
pairwise
from
textwrap
import
dedent
from
django_filters.rest_framework
import
DjangoFilterBackend
...
...
@@ -43,7 +42,6 @@ from django.db import IntegrityError
from
django.http
import
HttpResponseRedirect
,
JsonResponse
from
django.shortcuts
import
get_object_or_404
from
django.utils
import
timezone
from
django.utils.translation
import
gettext
as
_
from
program
import
filters
from
program.models
import
(
Category
,
...
...
@@ -89,59 +87,16 @@ from program.serializers import (
TypeSerializer
,
UserSerializer
,
)
from
program.services
import
get_timerange_timeslots
,
resolve_conflicts
from
program.services
import
(
get_timerange_timeslot_entries
,
make_schedule_entry
,
resolve_conflicts
,
)
from
program.utils
import
get_values
,
parse_date
logger
=
logging
.
getLogger
(
__name__
)
def
timeslot_entry
(
*
,
timeslot
:
TimeSlot
)
->
dict
:
"""
return a timeslot entry as a dict
"""
schedule
=
timeslot
.
schedule
show
=
timeslot
.
schedule
.
show
playlist_id
=
timeslot
.
playlist_id
title
=
show_name
=
f
"
{
show
.
name
}
{
_
(
'
REP
'
)
}
"
if
schedule
.
is_repetition
else
show
.
name
# we start and end as timezone naive datetime objects
start
=
timezone
.
make_naive
(
timeslot
.
start
).
strftime
(
"
%Y-%m-%dT%H:%M:%S
"
)
end
=
timezone
.
make_naive
(
timeslot
.
end
).
strftime
(
"
%Y-%m-%dT%H:%M:%S
"
)
return
{
"
end
"
:
end
,
"
id
"
:
timeslot
.
id
,
"
playlistId
"
:
playlist_id
,
# `Timeslot.repetition_of` is a foreign key that can be null
"
repetitionOfId
"
:
timeslot
.
repetition_of
.
id
if
timeslot
.
repetition_of
else
None
,
"
scheduleDefaultPlaylistId
"
:
schedule
.
default_playlist_id
,
"
scheduleId
"
:
schedule
.
id
,
"
showCategories
"
:
"
,
"
.
join
(
show
.
category
.
values_list
(
"
name
"
,
flat
=
True
)),
"
showDefaultPlaylistId
"
:
show
.
default_playlist_id
,
# `Show.funding_category` is a foreign key can be null
"
showFundingCategory
"
:
show
.
funding_category
.
name
if
show
.
funding_category_id
else
""
,
"
showHosts
"
:
"
,
"
.
join
(
show
.
hosts
.
values_list
(
"
name
"
,
flat
=
True
)),
"
showId
"
:
show
.
id
,
"
showLanguages
"
:
"
,
"
.
join
(
show
.
language
.
values_list
(
"
name
"
,
flat
=
True
)),
"
showMusicFocus
"
:
"
,
"
.
join
(
show
.
music_focus
.
values_list
(
"
name
"
,
flat
=
True
)),
"
showName
"
:
show_name
,
"
showTopics
"
:
"
,
"
.
join
(
show
.
topic
.
values_list
(
"
name
"
,
flat
=
True
)),
# `Show.type` is a foreign key that can be null
"
showType
"
:
show
.
type
.
name
if
show
.
type_id
else
""
,
"
start
"
:
start
,
"
title
"
:
title
,
}
def
gap_entry
(
*
,
gap_start
:
datetime
,
gap_end
:
datetime
)
->
dict
:
"""
return a virtual timeslot to fill the gap in between `gap_start` and `gap_end` as a dict
"""
return
{
"
end
"
:
gap_end
.
strftime
(
"
%Y-%m-%dT%H:%M:%S
"
),
"
start
"
:
gap_start
.
strftime
(
"
%Y-%m-%dT%H:%M:%S
"
),
"
virtual
"
:
True
,
}
@extend_schema_view
(
list
=
extend_schema
(
summary
=
"
List schedule for a specific date.
"
,
...
...
@@ -168,18 +123,12 @@ class APIDayScheduleViewSet(
end
=
start
+
timedelta
(
hours
=
24
)
timeslots
=
get_timerange_timeslots
(
start
,
end
).
select_related
(
"
schedule
"
)
schedule
=
[]
for
ts
in
timeslots
:
entry
=
{
"
start
"
:
ts
.
start
.
strftime
(
"
%Y-%m-%d_%H:%M:%S
"
),
"
end
"
:
ts
.
end
.
strftime
(
"
%Y-%m-%d_%H:%M:%S
"
),
"
title
"
:
ts
.
schedule
.
show
.
name
,
"
id
"
:
ts
.
schedule
.
show
.
id
,
}
include_virtual
=
request
.
GET
.
get
(
"
include_virtual
"
)
==
"
true
"
schedule
.
append
(
entry
)
schedule
=
[
make_schedule_entry
(
timeslot_entry
=
timeslot_entry
)
for
timeslot_entry
in
get_timerange_timeslot_entries
(
start
,
end
,
include_virtual
)
]
return
JsonResponse
(
schedule
,
safe
=
False
)
...
...
@@ -229,33 +178,9 @@ class APIPlayoutViewSet(
include_virtual
=
request
.
GET
.
get
(
"
include_virtual
"
)
==
"
true
"
timeslots
=
get_timerange_timeslots
(
schedule_start
,
schedule_end
).
select_related
(
"
schedule
"
)
schedule
=
[]
first_timeslot
=
timeslots
.
first
()
playout
=
get_timerange_timeslot_entries
(
schedule_start
,
schedule_end
,
include_virtual
)
if
include_virtual
and
first_timeslot
.
start
>
schedule_start
:
schedule
.
append
(
gap_entry
(
gap_start
=
schedule_start
,
gap_end
=
first_timeslot
.
start
))
for
current
,
upcoming
in
pairwise
(
timeslots
):
schedule
.
append
(
timeslot_entry
(
timeslot
=
current
))
if
include_virtual
and
current
.
end
!=
upcoming
.
start
:
schedule
.
append
(
gap_entry
(
gap_start
=
current
.
end
,
gap_end
=
upcoming
.
start
))
last_timeslot
=
timeslots
.
last
()
# we need to append the last timeslot to the schedule to complete it
if
last_timeslot
:
schedule
.
append
(
timeslot_entry
(
timeslot
=
last_timeslot
))
if
include_virtual
and
last_timeslot
.
end
<
schedule_end
:
schedule
.
append
(
gap_entry
(
gap_start
=
last_timeslot
.
end
,
gap_end
=
schedule_end
))
return
JsonResponse
(
schedule
,
safe
=
False
)
return
JsonResponse
(
playout
,
safe
=
False
)
@extend_schema_view
(
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment