Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
A
AKPlanning
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
GitLab community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Felix Blanke
AKPlanning
Commits
58bbe6a9
Commit
58bbe6a9
authored
1 year ago
by
Felix Blanke
Browse files
Options
Downloads
Patches
Plain Diff
Merge AK category slots
parent
216eecff
No related branches found
No related tags found
3 merge requests
!4
Draft: Add object import from JSON data
,
!3
Merge into fork's `main` branch
,
!2
Merge AK category slots
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
AKModel/availability/models.py
+8
-2
8 additions, 2 deletions
AKModel/availability/models.py
AKModel/models.py
+144
-18
144 additions, 18 deletions
AKModel/models.py
AKModel/views/ak.py
+11
-11
11 additions, 11 deletions
AKModel/views/ak.py
with
163 additions
and
31 deletions
AKModel/availability/models.py
+
8
−
2
View file @
58bbe6a9
...
@@ -151,9 +151,12 @@ class Availability(models.Model):
...
@@ -151,9 +151,12 @@ class Availability(models.Model):
if
not
other
.
overlaps
(
self
,
strict
=
False
):
if
not
other
.
overlaps
(
self
,
strict
=
False
):
raise
Exception
(
'
Only overlapping Availabilities can be merged.
'
)
raise
Exception
(
'
Only overlapping Availabilities can be merged.
'
)
return
Availability
(
avail
=
Availability
(
start
=
min
(
self
.
start
,
other
.
start
),
end
=
max
(
self
.
end
,
other
.
end
)
start
=
min
(
self
.
start
,
other
.
start
),
end
=
max
(
self
.
end
,
other
.
end
)
)
)
if
self
.
event
==
other
.
event
:
avail
.
event
=
self
.
event
return
avail
def
__or__
(
self
,
other
:
'
Availability
'
)
->
'
Availability
'
:
def
__or__
(
self
,
other
:
'
Availability
'
)
->
'
Availability
'
:
"""
Performs the merge operation: ``availability1 | availability2``
"""
"""
Performs the merge operation: ``availability1 | availability2``
"""
...
@@ -168,9 +171,12 @@ class Availability(models.Model):
...
@@ -168,9 +171,12 @@ class Availability(models.Model):
if
not
other
.
overlaps
(
self
,
False
):
if
not
other
.
overlaps
(
self
,
False
):
raise
Exception
(
'
Only overlapping Availabilities can be intersected.
'
)
raise
Exception
(
'
Only overlapping Availabilities can be intersected.
'
)
return
Availability
(
avail
=
Availability
(
start
=
max
(
self
.
start
,
other
.
start
),
end
=
min
(
self
.
end
,
other
.
end
)
start
=
max
(
self
.
start
,
other
.
start
),
end
=
min
(
self
.
end
,
other
.
end
)
)
)
if
self
.
event
==
other
.
event
:
avail
.
event
=
self
.
event
return
avail
def
__and__
(
self
,
other
:
'
Availability
'
)
->
'
Availability
'
:
def
__and__
(
self
,
other
:
'
Availability
'
)
->
'
Availability
'
:
"""
Performs the intersect operation: ``availability1 &
"""
Performs the intersect operation: ``availability1 &
...
...
This diff is collapsed.
Click to expand it.
AKModel/models.py
+
144
−
18
View file @
58bbe6a9
import
itertools
import
itertools
import
json
import
json
from
dataclasses
import
dataclass
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
from
typing
import
Iterable
from
typing
import
Iterable
...
@@ -14,6 +15,35 @@ from simple_history.models import HistoricalRecords
...
@@ -14,6 +15,35 @@ from simple_history.models import HistoricalRecords
from
timezone_field
import
TimeZoneField
from
timezone_field
import
TimeZoneField
@dataclass
class
OptimizerTimeslot
:
"""
Class describing a timeslot. Used to interface with an optimizer.
"""
avail
:
"
Availability
"
idx
:
int
constraints
:
set
[
str
]
def
merge
(
self
,
other
:
"
OptimizerTimeslot
"
)
->
"
OptimizerTimeslot
"
:
"""
Merge with other OptimizerTimeslot.
Creates a new OptimizerTimeslot object.
Its availability is constructed by merging the availabilities of self and other,
its constraints by taking the union of both constraint sets.
As an index, the index of self is used.
"""
avail
=
self
.
avail
.
merge_with
(
other
.
avail
)
constraints
=
self
.
constraints
.
union
(
other
.
constraints
)
# we simply use the index of result[-1]
return
OptimizerTimeslot
(
avail
=
avail
,
idx
=
self
.
idx
,
constraints
=
constraints
)
def
__repr__
(
self
)
->
str
:
return
f
"
(
{
self
.
avail
.
simplified
}
,
{
self
.
idx
}
,
{
self
.
constraints
}
)
"
TimeslotBlock
=
list
[
OptimizerTimeslot
]
class
Event
(
models
.
Model
):
class
Event
(
models
.
Model
):
"""
"""
An event supplies the frame for all Aks.
An event supplies the frame for all Aks.
...
@@ -164,8 +194,13 @@ class Event(models.Model):
...
@@ -164,8 +194,13 @@ class Event(models.Model):
)
)
def
_generate_slots_from_block
(
def
_generate_slots_from_block
(
self
,
start
:
datetime
,
end
:
datetime
,
slot_duration
:
timedelta
,
slot_index
:
int
=
0
self
,
)
->
Iterable
[
list
[
int
,
"
Availability
"
]]:
start
:
datetime
,
end
:
datetime
,
slot_duration
:
timedelta
,
slot_index
:
int
=
0
,
constraints
:
set
[
str
]
|
None
=
None
,
)
->
Iterable
[
TimeslotBlock
]:
"""
Discretize a time range into timeslots.
"""
Discretize a time range into timeslots.
Uses a uniform discretization into blocks of length `slot_duration`,
Uses a uniform discretization into blocks of length `slot_duration`,
...
@@ -179,8 +214,7 @@ class Event(models.Model):
...
@@ -179,8 +214,7 @@ class Event(models.Model):
:param slot_index: index of the first timeslot. Defaults to 0.
:param slot_index: index of the first timeslot. Defaults to 0.
:yield: Block of optimizer timeslots as the discretization result.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of tuples, each consisisting of the timeslot id
:ytype: list of TimeslotBlock
and its availability to indicate its start and duration.
"""
"""
# local import to prevent cyclic import
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
# pylint: disable=import-outside-toplevel
...
@@ -189,6 +223,9 @@ class Event(models.Model):
...
@@ -189,6 +223,9 @@ class Event(models.Model):
current_slot_start
=
start
current_slot_start
=
start
previous_slot_start
:
datetime
|
None
=
None
previous_slot_start
:
datetime
|
None
=
None
if
constraints
is
None
:
constraints
=
set
()
current_block
=
[]
current_block
=
[]
room_availabilities
=
list
({
room_availabilities
=
list
({
...
@@ -213,7 +250,9 @@ class Event(models.Model):
...
@@ -213,7 +250,9 @@ class Event(models.Model):
yield
current_block
yield
current_block
current_block
=
[]
current_block
=
[]
current_block
.
append
((
slot_index
,
slot
))
current_block
.
append
(
OptimizerTimeslot
(
avail
=
slot
,
idx
=
slot_index
,
constraints
=
constraints
)
)
previous_slot_start
=
current_slot_start
previous_slot_start
=
current_slot_start
slot_index
+=
1
slot_index
+=
1
...
@@ -224,44 +263,113 @@ class Event(models.Model):
...
@@ -224,44 +263,113 @@ class Event(models.Model):
return
slot_index
return
slot_index
def
uniform_time_slots
(
self
,
*
,
slots_in_an_hour
=
1.0
)
->
Iterable
[
list
[
int
,
"
Availability
"
]
]:
def
uniform_time_slots
(
self
,
*
,
slots_in_an_hour
:
float
=
1.0
)
->
Iterable
[
TimeslotBlock
]:
"""
Uniformly discretize the entire event into a single block of timeslots.
"""
Uniformly discretize the entire event into a single block of timeslots.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
Determines the discretization granularity.
Determines the discretization granularity.
:yield: Block of optimizer timeslots as the discretization result.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: a single list of tuples, each consisisting of the timeslot id
:ytype: a single list of TimeslotBlock
and its availability to indicate its start and duration.
"""
"""
all_category_constraints
=
AKCategory
.
create_category_constraints
(
AKCategory
.
objects
.
filter
(
event
=
self
).
all
()
)
yield
from
self
.
_generate_slots_from_block
(
yield
from
self
.
_generate_slots_from_block
(
start
=
self
.
start
,
start
=
self
.
start
,
end
=
self
.
end
,
end
=
self
.
end
,
slot_duration
=
timedelta
(
hours
=
1.0
/
slots_in_an_hour
),
slot_duration
=
timedelta
(
hours
=
1.0
/
slots_in_an_hour
),
constraints
=
all_category_constraints
,
)
)
def
default_time_slots
(
self
,
*
,
slots_in_an_hour
=
1.0
)
->
Iterable
[
list
[
int
,
"
Availability
"
]
]:
def
default_time_slots
(
self
,
*
,
slots_in_an_hour
:
float
=
1.0
)
->
Iterable
[
TimeslotBlock
]:
"""
Discretize
the
all default slots into
a
blocks of timeslots.
"""
Discretize all default slots into blocks of timeslots.
In the discretization each default slot corresponds to one block.
In the discretization each default slot corresponds to one block.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
:param slots_in_an_hour: The percentage of an hour covered by a single slot.
Determines the discretization granularity.
Determines the discretization granularity.
:yield: Block of optimizer timeslots as the discretization result.
:yield: Block of optimizer timeslots as the discretization result.
:ytype: list of tuples, each consisisting of the timeslot id
:ytype: list of TimeslotBlock
and its availability to indicate its start and duration.
"""
"""
slot_duration
=
timedelta
(
hours
=
1.0
/
slots_in_an_hour
)
slot_duration
=
timedelta
(
hours
=
1.0
/
slots_in_an_hour
)
slot_index
=
0
slot_index
=
0
for
block_slot
in
DefaultSlot
.
objects
.
filter
(
event
=
self
).
order_by
(
"
start
"
,
"
end
"
):
for
block_slot
in
DefaultSlot
.
objects
.
filter
(
event
=
self
).
order_by
(
"
start
"
,
"
end
"
):
# NOTE: We do not differentiate between different primary categories
category_constraints
=
AKCategory
.
create_category_constraints
(
block_slot
.
primary_categories
.
all
()
)
slot_index
=
yield
from
self
.
_generate_slots_from_block
(
slot_index
=
yield
from
self
.
_generate_slots_from_block
(
start
=
block_slot
.
start
,
start
=
block_slot
.
start
,
end
=
block_slot
.
end
,
end
=
block_slot
.
end
,
slot_duration
=
slot_duration
,
slot_duration
=
slot_duration
,
slot_index
=
slot_index
,
slot_index
=
slot_index
,
constraints
=
category_constraints
,
)
)
def
merge_blocks
(
self
,
blocks
:
Iterable
[
TimeslotBlock
]
)
->
Iterable
[
TimeslotBlock
]:
"""
Merge iterable of blocks together.
The timeslots of all blocks are grouped into maximal blocks.
Timeslots with the same start and end are identified with each other
and merged (cf `OptimizerTimeslot.merge`).
Throws a ValueError if any timeslots are overlapping but do not
share the same start and end, i.e. partial overlap is not allowed.
:param blocks: iterable of blocks to merge.
:return: iterable of merged blocks.
:rtype: iterable over lists of OptimizerTimeslot objects
"""
if
not
blocks
:
return
[]
# flatten timeslot iterables to single chain
timeslot_chain
=
itertools
.
chain
.
from_iterable
(
blocks
)
# sort timeslots according to start
timeslots
=
sorted
(
timeslot_chain
,
key
=
lambda
slot
:
slot
.
avail
.
start
)
if
not
timeslots
:
return
[]
all_blocks
=
[]
current_block
=
[
timeslots
[
0
]]
timeslots
=
timeslots
[
1
:]
for
slot
in
timeslots
:
if
current_block
and
slot
.
avail
.
overlaps
(
current_block
[
-
1
].
avail
,
strict
=
True
):
if
(
slot
.
avail
.
start
==
current_block
[
-
1
].
avail
.
start
and
slot
.
avail
.
end
==
current_block
[
-
1
].
avail
.
end
):
# the same timeslot -> merge
current_block
[
-
1
]
=
current_block
[
-
1
].
merge
(
slot
)
else
:
# partial overlap of interiors -> not supported
# TODO: Show comprehensive message in production
raise
ValueError
(
"
Partially overlapping timeslots are not supported!
"
f
"
(
{
current_block
[
-
1
].
avail
.
simplified
}
,
{
slot
.
avail
.
simplified
}
)
"
)
elif
not
current_block
or
slot
.
avail
.
overlaps
(
current_block
[
-
1
].
avail
,
strict
=
False
):
# only endpoints in intersection -> same block
current_block
.
append
(
slot
)
else
:
# no overlap at all -> new block
all_blocks
.
append
(
current_block
)
current_block
=
[
slot
]
if
current_block
:
all_blocks
.
append
(
current_block
)
return
all_blocks
def
schedule_from_json
(
self
,
schedule
:
str
)
->
None
:
def
schedule_from_json
(
self
,
schedule
:
str
)
->
None
:
"""
Load AK schedule from a json string.
"""
Load AK schedule from a json string.
...
@@ -275,9 +383,9 @@ class Event(models.Model):
...
@@ -275,9 +383,9 @@ class Event(models.Model):
slots_in_an_hour
=
schedule
[
"
input
"
][
"
timeslots
"
][
"
info
"
][
"
duration
"
]
slots_in_an_hour
=
schedule
[
"
input
"
][
"
timeslots
"
][
"
info
"
][
"
duration
"
]
timeslot_dict
=
{
timeslot_dict
=
{
slot
_
idx
:
slot
time
slot
.
idx
:
time
slot
for
block
in
self
.
default_time_slots
(
slots_in_an_hour
=
slots_in_an_hour
)
for
block
in
self
.
merge_blocks
(
self
.
default_time_slots
(
slots_in_an_hour
=
slots_in_an_hour
)
)
for
slot_idx
,
slot
in
block
for
time
slot
in
block
}
}
for
scheduled_slot
in
schedule
[
"
scheduled_aks
"
]:
for
scheduled_slot
in
schedule
[
"
scheduled_aks
"
]:
...
@@ -286,8 +394,8 @@ class Event(models.Model):
...
@@ -286,8 +394,8 @@ class Event(models.Model):
scheduled_slot
[
"
timeslot_ids
"
]
=
list
(
map
(
int
,
scheduled_slot
[
"
timeslot_ids
"
]))
scheduled_slot
[
"
timeslot_ids
"
]
=
list
(
map
(
int
,
scheduled_slot
[
"
timeslot_ids
"
]))
start_timeslot
=
timeslot_dict
[
min
(
scheduled_slot
[
"
timeslot_ids
"
])]
start_timeslot
=
timeslot_dict
[
min
(
scheduled_slot
[
"
timeslot_ids
"
])]
.
avail
end_timeslot
=
timeslot_dict
[
max
(
scheduled_slot
[
"
timeslot_ids
"
])]
end_timeslot
=
timeslot_dict
[
max
(
scheduled_slot
[
"
timeslot_ids
"
])]
.
avail
slot
.
start
=
start_timeslot
.
start
slot
.
start
=
start_timeslot
.
start
slot
.
duration
=
(
end_timeslot
.
end
-
start_timeslot
.
start
).
total_seconds
()
/
3600.0
slot
.
duration
=
(
end_timeslot
.
end
-
start_timeslot
.
start
).
total_seconds
()
/
3600.0
...
@@ -390,6 +498,20 @@ class AKCategory(models.Model):
...
@@ -390,6 +498,20 @@ class AKCategory(models.Model):
def
__str__
(
self
):
def
__str__
(
self
):
return
self
.
name
return
self
.
name
@staticmethod
def
create_category_constraints
(
categories
:
Iterable
[
"
AKCategory
"
])
->
set
[
str
]:
"""
Create a set of constraint strings from an AKCategory iterable.
:param categories: The iterable of categories to derive the constraint strings from.
:return: A set of category constraint strings, i.e. strings of the form
'
availability-cat-<cat.name>
'
.
:rtype: set of strings.
"""
return
{
f
"
availability-cat-
{
cat
.
name
}
"
for
cat
in
categories
}
class
AKTrack
(
models
.
Model
):
class
AKTrack
(
models
.
Model
):
"""
An AKTrack describes a set of semantically related AKs.
"""
An AKTrack describes a set of semantically related AKs.
...
@@ -821,6 +943,10 @@ class AKSlot(models.Model):
...
@@ -821,6 +943,10 @@ class AKSlot(models.Model):
for
owner
in
self
.
ak
.
owners
.
all
():
for
owner
in
self
.
ak
.
owners
.
all
():
data
[
"
time_constraints
"
].
extend
(
_owner_time_constraints
(
owner
))
data
[
"
time_constraints
"
].
extend
(
_owner_time_constraints
(
owner
))
if
self
.
ak
.
category
:
category_constraints
=
AKCategory
.
create_category_constraints
([
self
.
ak
.
category
])
data
[
"
time_constraints
"
].
extend
(
category_constraints
)
if
self
.
room
is
not
None
and
self
.
fixed
:
if
self
.
room
is
not
None
and
self
.
fixed
:
data
[
"
room_constraints
"
].
append
(
f
"
availability-room-
{
self
.
room
.
pk
}
"
)
data
[
"
room_constraints
"
].
append
(
f
"
availability-room-
{
self
.
room
.
pk
}
"
)
...
...
This diff is collapsed.
Click to expand it.
AKModel/views/ak.py
+
11
−
11
View file @
58bbe6a9
...
@@ -50,6 +50,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
...
@@ -50,6 +50,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context_object_name
=
"
slots
"
context_object_name
=
"
slots
"
title
=
_
(
"
AK JSON Export
"
)
title
=
_
(
"
AK JSON Export
"
)
def
_test_slot_contained
(
self
,
slot
:
Availability
,
availabilities
:
List
[
Availability
])
->
bool
:
def
_test_slot_contained
(
self
,
slot
:
Availability
,
availabilities
:
List
[
Availability
])
->
bool
:
return
any
(
availability
.
contains
(
slot
)
for
availability
in
availabilities
)
return
any
(
availability
.
contains
(
slot
)
for
availability
in
availabilities
)
...
@@ -69,7 +70,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
...
@@ -69,7 +70,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
and
self
.
_test_slot_contained
(
slot
,
availabilities
)
and
self
.
_test_slot_contained
(
slot
,
availabilities
)
)
)
def
get_queryset
(
self
):
def
get_queryset
(
self
):
return
super
().
get_queryset
().
order_by
(
"
ak__track
"
)
return
super
().
get_queryset
().
order_by
(
"
ak__track
"
)
...
@@ -110,38 +110,38 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
...
@@ -110,38 +110,38 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
if
(
values
:
=
AKSlot
.
objects
.
select_related
().
filter
(
ak__pk
=
ak_id
,
fixed
=
True
)).
exists
()
if
(
values
:
=
AKSlot
.
objects
.
select_related
().
filter
(
ak__pk
=
ak_id
,
fixed
=
True
)).
exists
()
}
}
for
block
in
self
.
event
.
default_time_slots
(
slots_in_an_hour
=
SLOTS_IN_AN_HOUR
):
for
block
in
self
.
event
.
merge_blocks
(
self
.
event
.
default_time_slots
(
slots_in_an_hour
=
SLOTS_IN_AN_HOUR
)
)
:
current_block
=
[]
current_block
=
[]
for
slot_index
,
slot
in
block
:
for
time
slot
in
block
:
time_constraints
=
[]
time_constraints
=
[]
if
self
.
event
.
reso_deadline
is
None
or
timeslot
.
avail
.
end
<
self
.
event
.
reso_deadline
:
if
self
.
event
.
reso_deadline
is
None
or
slot
.
end
<
self
.
event
.
reso_deadline
:
time_constraints
.
append
(
"
resolution
"
)
time_constraints
.
append
(
"
resolution
"
)
time_constraints
.
extend
([
time_constraints
.
extend
([
f
"
availability-ak-
{
ak_id
}
"
f
"
availability-ak-
{
ak_id
}
"
for
ak_id
,
availabilities
in
ak_availabilities
.
items
()
for
ak_id
,
availabilities
in
ak_availabilities
.
items
()
if
(
if
(
self
.
_test_add_constraint
(
slot
,
availabilities
)
self
.
_test_add_constraint
(
timeslot
.
avail
,
availabilities
)
or
self
.
_test_fixed_ak
(
ak_id
,
slot
,
ak_fixed
)
or
self
.
_test_fixed_ak
(
ak_id
,
timeslot
.
avail
,
ak_fixed
)
)
)
])
])
time_constraints
.
extend
([
time_constraints
.
extend
([
f
"
availability-person-
{
person_id
}
"
f
"
availability-person-
{
person_id
}
"
for
person_id
,
availabilities
in
person_availabilities
.
items
()
for
person_id
,
availabilities
in
person_availabilities
.
items
()
if
self
.
_test_add_constraint
(
slot
,
availabilities
)
if
self
.
_test_add_constraint
(
timeslot
.
avail
,
availabilities
)
])
])
time_constraints
.
extend
([
time_constraints
.
extend
([
f
"
availability-room-
{
room_id
}
"
f
"
availability-room-
{
room_id
}
"
for
room_id
,
availabilities
in
room_availabilities
.
items
()
for
room_id
,
availabilities
in
room_availabilities
.
items
()
if
self
.
_test_add_constraint
(
slot
,
availabilities
)
if
self
.
_test_add_constraint
(
timeslot
.
avail
,
availabilities
)
])
])
time_constraints
.
extend
(
timeslot
.
constraints
)
current_block
.
append
({
current_block
.
append
({
"
id
"
:
str
(
slot_inde
x
),
"
id
"
:
str
(
timeslot
.
id
x
),
"
info
"
:
{
"
info
"
:
{
"
start
"
:
slot
.
simplified
,
"
start
"
:
timeslot
.
avail
.
simplified
,
},
},
"
fulfilled_time_constraints
"
:
time_constraints
,
"
fulfilled_time_constraints
"
:
time_constraints
,
})
})
...
...
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