+
+
+
"""
@@ -2730,7 +2901,7 @@ class RotaBuilder(object):
temp = ""
for shift in self.get_shift_names_by_week_day(week, day):
if self.model.works[worker.id, week, day, shift].value > 0:
- shifts[d]=shift
+ shifts[d] = shift
return shifts
@@ -2738,12 +2909,12 @@ class RotaBuilder(object):
shifts = {}
for worker in self.workers:
- shifts[worker.name] = len([i for i in self.get_worker_shift_list(worker) if i != ""])
+ shifts[worker.name] = len(
+ [i for i in self.get_worker_shift_list(worker) if i != ""]
+ )
return shifts
-
-
def get_worker_shift_list(self, worker: Worker) -> List:
shifts = []
@@ -2822,14 +2993,17 @@ class RotaBuilder(object):
class NoActiveSites(Exception):
"""Raised when there are no active sites"""
+
pass
class NoWorkers(Exception):
"""Raised when there are no active sites"""
+
pass
class InvalidShift(Exception):
"""Raised when there are no active sites"""
- pass
\ No newline at end of file
+
+ pass
diff --git a/rota/workers.py b/rota/workers.py
index fb32173..3b52a62 100644
--- a/rota/workers.py
+++ b/rota/workers.py
@@ -1,7 +1,7 @@
import datetime
from collections import defaultdict
from typing import Iterable, List, Literal, Optional
-from pydantic import BaseModel, Extra
+from pydantic import BaseModel, Extra, validator
from rich.pretty import pprint
@@ -15,29 +15,51 @@ if TYPE_CHECKING:
from rota.shifts import RotaBuilder
days = Literal["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+whole_days = [
+ "sunday",
+ "monday",
+ "tuesday",
+ "wednesday",
+ "thursday",
+ "friday",
+ "saturday",
+]
+
class NotAvailableToWork(BaseModel):
date: datetime.date
reason: str = "unknown"
+
class NonWorkingDays(BaseModel):
day: days
start_date: datetime.date | None = None
end_date: datetime.date | None = None
+ @validator("day")
+ def day_in(cls, day):
+ for whole_day in whole_days:
+ if day.lower() in whole_day:
+ return whole_day[:3].capitalize()
+ raise ValueError("Invalid day")
+
+
class WorkRequests(BaseModel):
date: datetime.date
shift: str = ""
+
class PreferenceNotToWork(BaseModel):
date: datetime.date
reason: str = ""
+
class OutOfProgramme(BaseModel):
start_date: datetime.date
end_date: datetime.date
reason: str = ""
+
class Worker(BaseModel):
name: str
site: str
@@ -45,7 +67,7 @@ class Worker(BaseModel):
# rules is easier.
grade: int
# We can either have a user generated ID
- id: Optional[int]=None
+ id: Optional[int] = None
fte: int = 100
nwds: list[NonWorkingDays] = []
start_date: datetime.date | None = None
@@ -54,7 +76,7 @@ class Worker(BaseModel):
not_available_to_work: list[NotAvailableToWork] = []
pref_not_to_work: list[PreferenceNotToWork] = []
work_requests: list[WorkRequests] = []
- remote_site: str = "plymouth", # We set a default proc_site
+ remote_site: str = ("plymouth",) # We set a default proc_site
previous_shifts: dict = {}
shift_balance_extra: dict = {}
bank_holiday_extra: int = 0
@@ -63,27 +85,27 @@ class Worker(BaseModel):
class Config:
extra = Extra.allow
-# def __init__(
-# self,
-# #Rota: "RotaBuilder", # Aim to remove (workers should be independent of rotas)
-# ):
-#
-# self.name = name
-# self.site = site
-# self.grade = grade
-# self.fte = fte
-# self.nwd = nwd
-# self.pair = pair
-# self.shift_balance_extra = shift_balance_extra
-# self.bank_holiday_extra = bank_holiday_extra
-# self.start_date
-#
-# self.remote_site = remote_site
-#
-# self.previous_shifts = previous_shifts
-#
-#
-# # days_to_work = Rota.rota_days_length
+ # def __init__(
+ # self,
+ # #Rota: "RotaBuilder", # Aim to remove (workers should be independent of rotas)
+ # ):
+ #
+ # self.name = name
+ # self.site = site
+ # self.grade = grade
+ # self.fte = fte
+ # self.nwd = nwd
+ # self.pair = pair
+ # self.shift_balance_extra = shift_balance_extra
+ # self.bank_holiday_extra = bank_holiday_extra
+ # self.start_date
+ #
+ # self.remote_site = remote_site
+ #
+ # self.previous_shifts = previous_shifts
+ #
+ #
+ # # days_to_work = Rota.rota_days_length
def load_rota(self, Rota):
self.proportion_rota_to_work = 1
@@ -96,6 +118,9 @@ class Worker(BaseModel):
if self.start_date >= self.end_date:
raise ValueError("End date must be after start date")
+ if self.name == "Steph bailey":
+ print(self.start_date, self.end_date)
+
# if no start date default to the start of the rota
if self.start_date is None:
self.calculated_start_date = Rota.start_date
@@ -144,9 +169,7 @@ class Worker(BaseModel):
if isinstance(end_oop, datetime.date):
end_oop_date = end_oop
else:
- end_oop_date = datetime.datetime.strptime(
- end_oop, "%d/%m/%y"
- ).date()
+ end_oop_date = datetime.datetime.strptime(end_oop, "%d/%m/%y").date()
if start_oop_date >= end_oop_date:
raise ValueError("End OOP date must be after start date")
@@ -178,10 +201,7 @@ class Worker(BaseModel):
# loop throught dates converting to week / day combination
for item in self.pref_not_to_work:
- days_from_start = (
- item.date
- - Rota.start_date
- ).days
+ days_from_start = (item.date - Rota.start_date).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
# Weight the value to take into account the number of preferences
@@ -195,22 +215,20 @@ class Worker(BaseModel):
# print(not_available_to_work)
# loop throught dates converting to week / day combination
for item in self.not_available_to_work:
- days_from_start = (
- item.date
- - Rota.start_date
- ).days
+ days_from_start = (item.date - Rota.start_date).days
week = days_from_start // 7 + 1
day = Rota.days[(days_from_start % 7)]
Rota.unavailable_to_work.add((self.id, week, day))
Rota.unavailable_to_work_reason[(self.id, week, day)] = item.reason
for item in self.work_requests:
- days_from_start = (
- item.date
- - Rota.start_date
- ).days
- week = days_from_start // 7 + 1
- day = Rota.days[(days_from_start % 7)]
+ days_from_start = (item.date - Rota.start_date).days
+ week = days_from_start // 7 + 1
+ day = Rota.days[(days_from_start % 7)]
+ if item.shift == "*":
+ for shift in Rota.get_shifts_for_worker_site(self.site):
+ Rota.work_requests.add((self.id, week, day, shift.name))
+ else:
Rota.work_requests.add((self.id, week, day, item.shift))
# Calculate the proportion of the rota that is being worked
@@ -236,6 +254,9 @@ class Worker(BaseModel):
self.non_working_day_list.append((item.day, start_date, end_date))
+ if days_to_work < 1:
+ self.fte_adj = 0
+
def __lt__(self, other) -> bool:
return (self.site, self.grade, self.fte_adj, self.name) < (
other.site,
@@ -246,7 +267,11 @@ class Worker(BaseModel):
def __str__(self) -> str:
- nwds = ", ".join([str(i) for i, start, end in self.non_working_day_list]) if self.non_working_day_list is not None else ""
+ nwds = (
+ ", ".join([str(i) for i, start, end in self.non_working_day_list])
+ if self.non_working_day_list is not None
+ else ""
+ )
return "{} {} [{}] REMOTE SITE:{}".format(
self.name,
(self.site, self.grade, self.fte_adj),
diff --git a/run.py b/run.py
index 022b8c3..4d1df4f 100644
--- a/run.py
+++ b/run.py
@@ -78,10 +78,7 @@ Rota.constraint_options["max_weekends"] = 7
Rota.add_shifts(
SingleShift(
- ("exeter",),
- "exeter_twilight",
- 12.5,
- days[:4],
+ sites=("exeter",), name="exeter_twilight", length= 12.5, days=days[:4],
balance_offset=3,
constraints=["max_2_shifts_per_week"],
),
@@ -89,37 +86,25 @@ Rota.add_shifts(
("truro", "truro no proc"), "truro_twilight", 12.5, days[:4], balance_offset=3
),
SingleShift(
- ("torbay", "torbay twilights"),
- "torbay_twilight",
- 12.5,
- days[:4],
+ sites=("torbay", "torbay twilights"), name="torbay_twilight", length= 12.5, days=days[:4],
balance_offset=5,
assign_as_block=True,
),
SingleShift(
- ("plymouth", "plymouth_twilights"),
- "plymouth_twilight",
- 12.5,
- days[:5],
+ sites=("plymouth", "plymouth_twilights"), name="plymouth_twilight", length= 12.5, days=days[:5],
balance_offset=3,
workers_required=1,
constraints=["max_2_shifts_per_week"],
),
SingleShift(
- ("exeter",),
- "weekend_exeter",
- 12.5,
- days[4:],
+ sites=("exeter",), name="weekend_exeter", length= 12.5, days=days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
constraints=["preclear2", "postclear2"],
),
SingleShift(
- ("truro", "truro no proc"),
- "weekend_truro",
- 12.5,
- days[4:],
+ sites=("truro", "truro no proc"), name="weekend_truro", length= 12.5, days=days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
@@ -127,10 +112,7 @@ Rota.add_shifts(
# force_as_block_unless_nwd=True
),
SingleShift(
- ("torbay",),
- "weekend_torbay",
- 12.5,
- days[4:],
+ sites=("torbay",), name="weekend_torbay", length= 12.5, days=days[4:],
balance_offset=4,
rota_on_nwds=True,
force_as_block=True,
@@ -138,10 +120,7 @@ Rota.add_shifts(
# force_as_block_unless_nwd=True
),
SingleShift(
- ("plymouth",),
- "weekend_plymouth1",
- 8,
- days[5:],
+ sites=("plymouth",), name="weekend_plymouth1", length= 8, days=days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
@@ -149,10 +128,7 @@ Rota.add_shifts(
constraints=["preclear2", "postclear2"],
),
SingleShift(
- ("plymouth",),
- "weekend_plymouth2",
- 8,
- days[5:],
+ sites=("plymouth",), name="weekend_plymouth2", length= 8, days=days[5:],
balance_offset=4,
workers_required=1,
rota_on_nwds=True,
@@ -160,10 +136,7 @@ Rota.add_shifts(
constraints=["preclear2", "postclear2"],
),
SingleShift(
- ("plymouth", "plymouth_twilights"),
- "plymouth_bank_holidays",
- 8,
- days[5:],
+ sites=("plymouth", "plymouth_twilights"), name="plymouth_bank_holidays", length= 8, days=days[5:],
balance_offset=2,
workers_required=1,
rota_on_nwds=True,
@@ -171,10 +144,7 @@ Rota.add_shifts(
bank_holidays_only=True,
),
SingleShift(
- (sites[:-1]),
- "night_weekday",
- 12.25,
- days[:4],
+ sites=(sites[:-1]), name="night_weekday", length= 12.25, days=days[:4],
balance_offset=4.9,
balance_weighting=1,
# hard_constrain_shift=False,
@@ -184,10 +154,7 @@ Rota.add_shifts(
constraints=["night", "preclear2", "postclear2"],
),
SingleShift(
- (sites[:-1]),
- "night_weekend",
- 12.25,
- days[4:],
+ sites=(sites[:-1]), name="night_weekend", length= 12.25, days=days[4:],
balance_offset=3.9,
balance_weighting=1,
# hard_constrain_shift=False,
diff --git a/test/rota_constraints/__init__.py b/test/rota_constraints/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/rota_constraints/test_avoid_grades_by_week.py b/test/rota_constraints/test_avoid_grades_by_week.py
new file mode 100644
index 0000000..88e6e54
--- /dev/null
+++ b/test/rota_constraints/test_avoid_grades_by_week.py
@@ -0,0 +1,195 @@
+import datetime
+import pytest
+from rota.shifts import InvalidShift, NoWorkers, RotaBuilder, SingleShift, days
+
+from rota.workers import Worker
+
+import itertools
+
+
+def generate_basic_rota(weeks_to_rota=9):
+ start_date = datetime.date(2022, 3, 7)
+
+ Rota = RotaBuilder(
+ start_date,
+ weeks_to_rota=weeks_to_rota,
+ )
+
+ # Add a few workers
+ Rota.add_workers(
+ [
+ Worker(name="worker1", site="group1", grade=1),
+ Worker(name="worker2", site="group1", grade=2),
+ Worker(name="worker3", site="group1", grade=3),
+ ]
+ )
+
+ return Rota
+
+
+def date_generator(from_date, days):
+ n = 0
+ while True:
+ yield from_date
+
+ n = n + 1
+ if n >= days:
+ break
+ from_date = from_date + datetime.timedelta(days=1)
+
+
+class TestWorkerRequests:
+ def test_avoid_grade_constraint(self):
+ Rota = generate_basic_rota()
+
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="a", length= 12.5, days=days,
+ workers_required=2,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.add_grade_constraint_by_week([1], [1,2,3], ["a"])
+ Rota.add_grade_constraint_by_week([2], [4,5,6], ["a"])
+ Rota.add_grade_constraint_by_week([3], [7,8,9], ["a"])
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("avoid_grade_constraint")
+
+ shift_summary = Rota.get_shift_summary_dict()
+ for worker_name in shift_summary:
+ worker_shifts = shift_summary[worker_name]
+
+ worker = Rota.get_worker_by_name(worker_name)
+
+ shift_string = Rota.get_worker_shift_list_string(worker)
+
+ match worker_name:
+ case "worker1":
+ assert shift_string.startswith("-"*21)
+ case "worker2":
+ assert shift_string.startswith(21*"a"+"-"*21)
+ case "worker3":
+ assert shift_string.endswith("-"*21)
+
+
+ def test_avoid_grade_constraint_invalid_shift(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="a", length= 12.5, days=days,
+ workers_required=1,
+ force_as_block=False,
+ ),
+ )
+ with pytest.raises(ValueError):
+
+ Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="b", length= 12.5, days=days,
+ workers_required=1,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.add_grade_constraint_by_week([1], [1,2,3], ["b"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "ok"
+
+
+ def test_avoid_grade_constraint_invalid_grade(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="a", length= 12.5, days=days,
+ workers_required=2,
+ force_as_block=False,
+ ),
+ )
+ with pytest.raises(ValueError):
+
+ Rota.add_grade_constraint_by_week([4], [1,2,3], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+ Rota.add_worker(
+ Worker(name="worker4", site="group1", grade=4),
+ )
+
+
+ Rota.add_grade_constraint_by_week([4], [1,2,3], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "ok"
+
+ def test_avoid_grade_constraint_multiple_grades(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="a", length= 12.5, days=days,
+ workers_required=1,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.add_grade_constraint_by_week([1,2], [1,2,3], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "ok"
+
+ Rota.add_grade_constraint_by_week([3], [1,2,3], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "warning"
+
+ def test_avoid_grade_constraint_multiple_shifts(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1",), name="a", length= 12.5, days=days,
+ workers_required=1,
+ force_as_block=False,
+ ),
+ SingleShift(
+ sites=("group1",), name="b", length= 12.5, days=days,
+ workers_required=1,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.add_grade_constraint_by_week([1], [1,2,3], ["a", "b"])
+ Rota.add_grade_constraint_by_week([2], [4,5,6], ["a", "b"])
+ Rota.add_grade_constraint_by_week([3], [7,8,9], ["a", "b"])
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("avoid_grade_constraint")
+
+ shift_summary = Rota.get_shift_summary_dict()
+ for worker_name in shift_summary:
+ worker_shifts = shift_summary[worker_name]
+
+ worker = Rota.get_worker_by_name(worker_name)
+
+ shift_string = Rota.get_worker_shift_list_string(worker)
+
+ match worker_name:
+ case "worker1":
+ assert shift_string.startswith("-"*21)
+ case "worker2":
+ assert "-"*21 in shift_string # hacky
+ case "worker3":
+ assert shift_string.endswith("-"*21)
+
+ Rota.add_grade_constraint_by_week([2], [7,8,9], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "ok"
+
+ Rota.add_grade_constraint_by_week([1], [7,8,9], ["a"])
+ Rota.build_and_solve(options={"ratio": 0.000})
+ assert Rota.results.solver.status == "warning"
\ No newline at end of file
diff --git a/test/test_general_balancing.py b/test/test_general_balancing.py
index 836a61f..eb9c015 100644
--- a/test/test_general_balancing.py
+++ b/test/test_general_balancing.py
@@ -15,14 +15,16 @@ def generate_basic_rota(weeks_to_rota=10):
Rota.constraint_options["balance_weekends"] = True
# Add a few workers
- Rota.add_workers([
- Worker(name="worker1", site="group1", grade=1),
- Worker(name="worker2", site="group1", grade=1),
- Worker(name="worker3", site="group2", grade=1),
- #Worker(name="worker4", site="group2", grade=1),
- #Worker(name="worker5", site="group2", grade=1),
- #Worker(name="worker6", site="group2", grade=1, fte=50),
- ])
+ Rota.add_workers(
+ [
+ Worker(name="worker1", site="group1", grade=1),
+ Worker(name="worker2", site="group1", grade=1),
+ Worker(name="worker3", site="group2", grade=1),
+ # Worker(name="worker4", site="group2", grade=1),
+ # Worker(name="worker5", site="group2", grade=1),
+ # Worker(name="worker6", site="group2", grade=1, fte=50),
+ ]
+ )
return Rota
@@ -33,22 +35,22 @@ class TestBalancing:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days,
balance_offset=2,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="b",
+ length=12.5,
+ days=days,
balance_offset=2,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -68,30 +70,30 @@ class TestBalancing:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=["group1", "group2"],
+ name="a",
+ length=12.5,
+ days=days,
balance_weighting=4,
workers_required=1,
force_as_block=False,
- constraint=["preclear2", "postclear2"],
+ constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="b",
+ length=12.5,
+ days=days,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
SingleShift(
- ("group1", "group2"),
- "c",
- 12.5,
- days[0],
+ sites=("group1", "group2"),
+ name="c",
+ length=12.5,
+ days=days[0],
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -103,39 +105,39 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
- assert worker_shifts["a"] in (46,47,48)
- #assert worker_shifts["b"] in (22, 23, 24, 25)
+ assert worker_shifts["a"] in (46, 47, 48)
+ # assert worker_shifts["b"] in (22, 23, 24, 25)
def test_weighted_shift_balancing2(self):
Rota = generate_basic_rota(23)
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
- #balance_weighting=4,
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days,
+ # balance_weighting=4,
workers_required=1,
force_as_block=False,
- #constraint=["preclear2", "postclear2"],
+ # constraint=["preclear2", "postclear2"],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="b",
+ length=12.5,
+ days=days,
balance_weighting=4,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
SingleShift(
- ("group1", "group2"),
- "c",
- 12.5,
- days[0],
+ sites=("group1", "group2"),
+ name="c",
+ length=12.5,
+ days=days[0],
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -147,41 +149,41 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
- #assert worker_shifts["a"] in (46,47,48)
- #assert worker_shifts["c"] in (46,47,48)
- assert worker_shifts["b"] in (52,53,54,55)
+ # assert worker_shifts["a"] in (46,47,48)
+ # assert worker_shifts["c"] in (46,47,48)
+ assert worker_shifts["b"] in (52, 53, 54, 55)
def test_weighted_shift_balancing3(self):
Rota = generate_basic_rota(23)
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
- #balance_weighting=4,
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days,
+ # balance_weighting=4,
workers_required=1,
force_as_block=False,
- #constraint=["preclear2", "postclear2"],
+ # constraint=["preclear2", "postclear2"],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="b",
+ length=12.5,
+ days=days,
balance_weighting=4,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
SingleShift(
- ("group1", "group2"),
- "c",
- 12.5,
- days[0],
+ sites=("group1", "group2"),
+ name="c",
+ length=12.5,
+ days=days[0],
balance_weighting=4,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -193,9 +195,9 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
- #assert worker_shifts["a"] in (46,47,48)
- assert worker_shifts["c"] in (7,8)
- assert worker_shifts["b"] in (52,53,54,55)
+ # assert worker_shifts["a"] in (46,47,48)
+ assert worker_shifts["c"] in (7, 8)
+ assert worker_shifts["b"] in (52, 53, 54, 55)
def test_weighted_shift_balancing4(self):
Rota = generate_basic_rota(10)
@@ -206,25 +208,25 @@ class TestBalancing:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days,
balance_offset=99,
balance_weighting=8,
workers_required=2,
force_as_block=False,
- #constraint=["preclear2", "postclear2"],
+ # constraint=["preclear2", "postclear2"],
),
SingleShift(
- ("group2","group3"),
- "b",
- 12.5,
- days,
+ sites=("group2", "group3"),
+ name="b",
+ length=12.5,
+ days=days,
balance_offset=99,
- #balance_weighting=4,
+ # balance_weighting=4,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -236,7 +238,7 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
- assert worker_shifts["a"] in (0, 46,47,48)
+ assert worker_shifts["a"] in (0, 46, 47, 48)
def test_weighted_shift_balancing5(self):
Rota = generate_basic_rota(10)
@@ -247,25 +249,25 @@ class TestBalancing:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days,
balance_offset=99,
- #balance_weighting=8,
+ # balance_weighting=8,
workers_required=2,
force_as_block=False,
- #constraint=["preclear2", "postclear2"],
+ # constraint=["preclear2", "postclear2"],
),
SingleShift(
- ("group2","group3"),
- "b",
- 12.5,
- days,
+ sites=("group2", "group3"),
+ name="b",
+ length=12.5,
+ days=days,
balance_offset=99,
balance_weighting=4,
workers_required=1,
- force_as_block=False
+ force_as_block=False,
),
)
@@ -277,4 +279,4 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
- assert worker_shifts["b"] in (0, 35)
\ No newline at end of file
+ assert worker_shifts["b"] in (0, 35)
diff --git a/test/test_nwds.py b/test/test_nwds.py
index 8e20fda..fe343ba 100644
--- a/test/test_nwds.py
+++ b/test/test_nwds.py
@@ -32,25 +32,19 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
# balance_offset=10,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=2,
),
)
self.Rota.build_and_solve(options={"ratio": 0.10})
- self.Rota.export_rota_to_html("nwd")
+ self.Rota.export_rota_to_html("nwd1")
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
@@ -86,10 +80,7 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
@@ -131,10 +122,7 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
@@ -192,10 +180,7 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
@@ -234,10 +219,7 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=2,
force_as_block_unless_nwd=True,
@@ -290,20 +272,14 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=2,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
balance_offset=10,
workers_required=4,
force_as_block_unless_nwd=True,
@@ -359,20 +335,14 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
force_as_block_unless_nwd=True,
assign_as_block=True,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
balance_offset=10,
workers_required=2,
force_as_block_unless_nwd=True,
diff --git a/test/test_remote.py b/test/test_remote.py
index 17b75b1..f8f3553 100644
--- a/test/test_remote.py
+++ b/test/test_remote.py
@@ -30,14 +30,11 @@ class TestRemoteRotas:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=1,
balance_offset=100,
force_as_block=False,
- constraint=[("require_remote_site_presence_week", ("group2", 2))],
+ constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 2)}],
),
)
@@ -69,20 +66,14 @@ class TestRemoteRotas:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days[:3],
+ sites=("group1", "group2"), name="a", length= 12.5, days=days[:3],
workers_required=1,
balance_offset=10,
force_as_block=True,
- constraint=[("require_remote_site_presence_week", ("group2", 1))],
+ constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 1)}],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days[3:],
+ sites=("group1", "group2"), name="b", length= 12.5, days=days[3:],
workers_required=1,
balance_offset=10,
force_as_block=False,
@@ -135,20 +126,14 @@ class TestRemoteRotas:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days[:3],
+ sites=("group1", "group2"), name="a", length= 12.5, days=days[:3],
workers_required=1,
balance_offset=10,
force_as_block=False,
- constraint=[("require_remote_site_presence", ("group2", 1))],
+ constraint=[{"name":"require_remote_site_presence_week", "options":("group2", 1)}],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days[3:],
+ sites=("group1", "group2"), name="b", length= 12.5, days=days[3:],
workers_required=1,
balance_offset=10,
force_as_block=False,
diff --git a/test/test_rota.py b/test/test_rota.py
index 952d40a..109dcac 100644
--- a/test/test_rota.py
+++ b/test/test_rota.py
@@ -1,4 +1,5 @@
from copy import deepcopy
+from re import S
from black import main
import pytest
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
@@ -17,7 +18,6 @@ class TestDemoRota:
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
- max_weekend_frequency=1,
)
Rota.constraint_options["max_shifts_per_week"] = 5
@@ -32,22 +32,22 @@ class TestDemoRota:
Rota.constraint_options["balance_bank_holidays"] = False
# Add a few workers
- worker1 = Worker(name= "worker1", site= "group1", grade= 1)
- worker2 = Worker(name= "worker2", site= "group1", grade= 1)
- worker3 = Worker(name= "worker3", site= "group1", grade= 1)
- worker4 = Worker(name= "worker4", site= "group1", grade= 1)
- worker5 = Worker(name= "worker5", site= "group2", grade= 1, fte=60)
- worker6 = Worker(name= "worker6", site= "group2", grade= 1, fte=40)
+ worker1 = Worker(name="worker1", site="group1", grade=1)
+ worker2 = Worker(name="worker2", site="group1", grade=1)
+ worker3 = Worker(name="worker3", site="group1", grade=1)
+ worker4 = Worker(name="worker4", site="group1", grade=1)
+ worker5 = Worker(name="worker5", site="group2", grade=1, fte=60)
+ worker6 = Worker(name="worker6", site="group2", grade=1, fte=40)
Rota.add_workers((worker1, worker2, worker3, worker4, worker5, worker6))
# Add a weekday and weekend shift
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "weekday",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="weekday",
+ length=12.5,
+ days=days[:5],
balance_offset=1,
workers_required=1,
# We use a different balance weighting for each shift as otherwise
@@ -55,10 +55,10 @@ class TestDemoRota:
# balance_weighting=0.6,
),
SingleShift(
- ("group1",),
- "weekend",
- 12.5,
- days[5:],
+ sites=("group1",),
+ name="weekend",
+ length=12.5,
+ days=days[5:],
balance_offset=4,
# balance_weighting=0.5
),
@@ -191,7 +191,6 @@ class TestDemoRotaNights:
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
- max_weekend_frequency=1,
)
Rota.constraint_options["max_shifts_per_week"] = 5
@@ -223,12 +222,12 @@ class TestDemoRotaNights:
# Add a weekday and weekend shift
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "night_weekday",
- 12.5,
- days[:4],
+ sites=("group1", "group2"),
+ name="night_weekday",
+ length=12.5,
+ days=days[:4],
workers_required=1,
- constraint=["preclear2", "postclear2"],
+ constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
# constraint=["night"],
force_as_block=True
# We use a different balance weighting for each shift as otherwise
@@ -236,20 +235,20 @@ class TestDemoRotaNights:
# balance_weighting=0.6,
),
SingleShift(
- ("group1", "group2"),
- "night_weekend",
- 12.5,
- days[4:],
+ sites=("group1", "group2"),
+ name="night_weekend",
+ length=12.5,
+ days=days[4:],
# balance_weighting=0.5
- constraint=["preclear2", "postclear2"],
+ constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
# constraint=["night"],
force_as_block=True,
),
SingleShift(
- ("group1", "group2"),
- "twilight",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="twilight",
+ length=12.5,
+ days=days[:5],
# balance_weighting=0.5
workers_required=2,
# constraint=["preclear2", "postclear2"],
@@ -271,11 +270,9 @@ class TestDemoRotaNights:
Rota.export_rota_to_html("test3")
-
def test_start_date(self):
assert self.Rota.start_date == self.start_date
-
def test_night_assignment(self):
shift_summary = self.Rota.get_shift_summary_dict()
@@ -285,14 +282,14 @@ class TestDemoRotaNights:
worker = self.Rota.get_worker_by_name(worker_name)
if worker.fte == 40:
- assert worker_shifts["night_weekday"] in (0,4)
- assert worker_shifts["night_weekend"] in (0,3)
+ assert worker_shifts["night_weekday"] in (0, 4)
+ assert worker_shifts["night_weekend"] in (0, 3)
elif worker.fte == 60:
- assert worker_shifts["night_weekday"] in (0,4,8)
- assert worker_shifts["night_weekend"] in (0,3,6)
+ assert worker_shifts["night_weekday"] in (0, 4, 8)
+ assert worker_shifts["night_weekend"] in (0, 3, 6)
else:
- assert worker_shifts["night_weekday"] in (4,8)
- assert worker_shifts["night_weekend"] in (3,6)
+ assert worker_shifts["night_weekday"] in (4, 8)
+ assert worker_shifts["night_weekend"] in (3, 6)
class TestDemoRotaClear:
@@ -303,7 +300,6 @@ class TestDemoRotaClear:
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
- max_weekend_frequency=1,
)
Rota.constraint_options["max_shifts_per_month"] = 20
@@ -323,30 +319,30 @@ class TestDemoRotaClear:
def test_preclear(self):
self.Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="a",
+ length=12.5,
+ days=days[:5],
balance_offset=40,
workers_required=2,
- constraint=["preclear"],
+ constraint=[{"name": "preclear"}],
),
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="b",
+ length=12.5,
+ days=days[:5],
balance_offset=40,
workers_required=2,
),
SingleShift(
- ("group1", "group2"),
- "c",
- 12.5,
- days[3],
+ sites=("group1", "group2"),
+ name="c",
+ length=12.5,
+ days=days[3],
balance_offset=40,
workers_required=1,
- constraint=["preclear2", "postclear2"],
+ constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
),
)
@@ -389,7 +385,6 @@ class TestDemoRotaShiftConstraints:
start_date,
weeks_to_rota=weeks_to_rota,
balance_offset_modifier=1,
- max_weekend_frequency=1,
)
# Rota.constraint_options["max_shifts_per_week"] = 5
# Rota.constraint_options["max_shifts_per_month"] = 20
@@ -399,10 +394,10 @@ class TestDemoRotaShiftConstraints:
Rota.add_workers((worker1, worker2))
Rota.add_shifts(
SingleShift(
- ("group1",),
- "a",
- 12.5,
- days,
+ sites=("group1",),
+ name="a",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=1,
),
@@ -479,14 +474,14 @@ class TestDemoRotaBalanceShiftSites:
def test_balance_blocks_across_2_groups(self):
self.Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "weekday_night",
- 12.5,
- days[:4],
+ sites=("group1", "group2"),
+ name="weekday_night",
+ length=12.5,
+ days=days[:4],
balance_offset=40,
workers_required=2,
force_as_block=True,
- constraint=["balance_across_groups"],
+ constraint=[{"name": "balance_across_groups"}],
),
)
self.Rota.constraint_options["balance_nights_across_sites"] = False
@@ -515,14 +510,14 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.shifts = []
self.Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "weekday_night",
- 12.5,
- days[:4],
+ sites=("group1", "group2"),
+ name="weekday_night",
+ length=12.5,
+ days=days[:4],
balance_offset=40,
workers_required=3,
force_as_block=True,
- constraint=["balance_across_groups"],
+ constraint=[{"name": "balance_across_groups"}],
),
)
self.Rota.constraint_options["balance_nights_across_sites"] = False
@@ -549,14 +544,14 @@ class TestDemoRotaBalanceShiftSites:
self.Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days[:4],
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days[:4],
balance_offset=40,
workers_required=3,
force_as_block=True,
- constraint=["balance_across_groups"],
+ constraint=[{"name": "balance_across_groups"}],
),
)
self.Rota.constraint_options["balance_nights_across_sites"] = False
@@ -580,8 +575,7 @@ class TestDemoRotaBalanceShiftSites:
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
-
-class TestLimitConstraints:
+def generate_limit_constraint_rota():
weeks_to_rota = 8
start_date = datetime.date(2022, 3, 7)
@@ -603,30 +597,41 @@ class TestLimitConstraints:
Rota.shifts = []
+ return Rota
+
+class TestLimitConstraints:
+
def test_constraint_limit_grades(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
- balance_offset=40,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
+ balance_offset=60,
workers_required=4,
force_as_block=True,
- constraint=[("limit_grade_number", {2: 1})],
+ constraint=[{"name": "limit_grade_number", "options": {2: 1}}],
),
)
- # self.Rota.constraint_options["balance_nights_across_sites"] = False
- # self.Rota.constraint_options["balance_shifts_over_workers"] = False
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.00})
+ # Rota.constraint_options["balance_nights_across_sites"] = False
+ # Rota.constraint_options["balance_shifts_over_workers"] = False
+ #Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
+
+ Rota.export_rota_to_html("constraint_limit_grades")
+
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
+
+ grade_workers = Rota.get_workers_by_grade()
- grade_workers = self.Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -636,35 +641,33 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
-
def test_constraint_limit_grades2(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
- constraint=[("limit_grade_number", {3: 2})],
+ constraint=[{"name": "limit_grade_number", "options": {3: 2}}],
),
)
- # self.Rota.constraint_options["balance_nights_across_sites"] = False
- # self.Rota.constraint_options["balance_shifts_over_workers"] = False
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ # Rota.constraint_options["balance_nights_across_sites"] = False
+ # Rota.constraint_options["balance_shifts_over_workers"] = False
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
- self.Rota.export_rota_to_html("test9")
+ Rota.export_rota_to_html("test9")
- grade_workers = self.Rota.get_workers_by_grade()
+ grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -674,35 +677,51 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_limit_grades3(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+
+ Rota.workers = []
+
+ worker1 = Worker(name="worker1", site="group1", grade=2, remote_site="group1")
+ worker2 = Worker(name="worker2", site="group1", grade=2, remote_site="group1")
+ worker3 = Worker(name="worker3", site="group1", grade=2, remote_site="group1")
+ worker4 = Worker(name="worker4", site="group3", grade=3, remote_site="group2")
+ worker5 = Worker(name="worker5", site="group3", grade=3, remote_site="group2")
+ worker6 = Worker(name="worker6", site="group3", grade=3, remote_site="group2")
+ worker7 = Worker(name="worker7", site="group3", grade=2, remote_site="group2")
+
+ Rota.add_workers((worker1, worker2, worker3, worker4))
+ Rota.add_workers((worker5, worker6))
+
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
- balance_offset=40,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
+ balance_offset=60,
workers_required=4,
force_as_block=True,
- constraint=[("limit_grade_number", {2: 3, 3: 1})],
+ constraint=[{"name": "limit_grade_number", "options": {2: 3, 3: 1}},]
),
)
- # self.Rota.constraint_options["balance_nights_across_sites"] = False
- # self.Rota.constraint_options["balance_shifts_over_workers"] = False
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ # Rota.constraint_options["balance_nights_across_sites"] = False
+ # Rota.constraint_options["balance_shifts_over_workers"] = False
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.01})
- self.Rota.export_rota_to_html("test9")
+ Rota.export_rota_to_html("test9")
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
- grade_workers = self.Rota.get_workers_by_grade()
+ grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -712,55 +731,57 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
def test_constraint_limit_grades4(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
- constraint=[("limit_grade_number", {2: 4, 3: 0})],
+ constraint=[{"name": "limit_grade_number", "options": {2: 4, 3: 0}}],
),
)
- # self.Rota.constraint_options["balance_nights_across_sites"] = False
- # self.Rota.constraint_options["balance_shifts_over_workers"] = False
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ # Rota.constraint_options["balance_nights_across_sites"] = False
+ # Rota.constraint_options["balance_shifts_over_workers"] = False
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
- assert self.Rota.results.solver.termination_condition == "infeasible"
+ assert Rota.results.solver.termination_condition == "infeasible"
def test_constraint_minimum_grades(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=1,
force_as_block=True,
- constraint=[("minimum_grade_number", (3, 1))],
+ constraint=[{"name": "minimum_grade_number", "options": (3, 1)}],
),
)
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
- self.Rota.export_rota_to_html("test9")
+ Rota.export_rota_to_html("test9")
- grade_workers = self.Rota.get_workers_by_grade()
+ assert Rota.results.solver.status == "ok"
+
+ grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -770,33 +791,33 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
- assert self.Rota.results.solver.status == "ok"
def test_constraint_minimum_grades2(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
- constraint=[("minimum_grade_number", (3, 3))],
+ constraint=[{"name": "minimum_grade_number", "options": (3, 3)}],
),
)
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
- self.Rota.export_rota_to_html("test9")
+ Rota.export_rota_to_html("test9")
- grade_workers = self.Rota.get_workers_by_grade()
+ grade_workers = Rota.get_workers_by_grade()
for grade in grade_workers:
shift_patterns = []
for w in grade_workers[grade]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -806,71 +827,79 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("weekday_night") == limit
- assert self.Rota.results.solver.status == "ok"
+ assert Rota.results.solver.status == "ok"
def test_constraint_minimum_grades3(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
- constraint=[("minimum_grade_number", (3, 0))],
+ constraint=[{"name": "minimum_grade_number", "options": (3, 0)}],
),
)
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.build_and_solve(options={"ratio": 0.1})
- self.Rota.export_rota_to_html("test9")
+ Rota.export_rota_to_html("test9")
- assert self.Rota.results.solver.status == "ok"
+ assert Rota.results.solver.status == "ok"
def test_constraint_minimum_grades_no_valid_worker(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "weekday_night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="weekday_night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=4,
force_as_block=True,
- constraint=[("minimum_grade_number", (4, 1))],
+ constraint=[{"name": "minimum_grade_number", "options": (4, 1)}],
),
)
- self.Rota.constraint_options["balance_shifts_quadratic"] = True
+ Rota.constraint_options["balance_shifts_quadratic"] = True
with pytest.raises(ValueError):
- self.Rota.build_and_solve(options={"ratio": 0.1})
+ Rota.build_and_solve(options={"ratio": 0.1})
def test_constraint_require_remote_site_presence_week(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="night",
+ length=12.5,
+ days=days,
balance_offset=40,
workers_required=2,
force_as_block=True,
- constraint=[("require_remote_site_presence_week", ("group1", 2))],
+ constraint=[
+ {
+ "name": "require_remote_site_presence_week",
+ "options": ("group1", 2),
+ }
+ ],
),
)
- self.Rota.build_and_solve(options={"ratio": 0.00})
- self.Rota.export_rota_to_html("remote1")
+ Rota.build_and_solve(options={"ratio": 0.00})
+ Rota.export_rota_to_html("remote1")
- group_workers = self.Rota.get_workers_by_group()
+ group_workers = Rota.get_workers_by_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -880,67 +909,79 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("night") == limit
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence_week2(self):
- self.Rota.constraint_options["minimise_shift_diffs"] = False
- self.Rota.constraint_options["balance_shifts_quadratic"] = False
- self.Rota.constraint_options["balance_shift"] = False
- self.Rota.constraint_options["balance_nights_across_sites"] = True
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.constraint_options["minimise_shift_diffs"] = False
+ Rota.constraint_options["balance_shifts_quadratic"] = False
+ Rota.constraint_options["balance_shift"] = False
+ Rota.constraint_options["balance_nights_across_sites"] = True
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="night",
+ length=12.5,
+ days=days,
balance_offset=20,
workers_required=3,
force_as_block=True,
- constraint=[("require_remote_site_presence_week", ("group2", 2))],
+ constraint=[
+ {
+ "name": "require_remote_site_presence_week",
+ "options": ("group1", 2),
+ }
+ ],
),
)
- self.Rota.build_and_solve(options={"ratio": 0.00})
- self.Rota.export_rota_to_html("remote2")
+ Rota.build_and_solve(options={"ratio": 0.00})
+ Rota.export_rota_to_html("remote2")
- group_workers = self.Rota.get_workers_by_remote_group()
+ group_workers = Rota.get_workers_by_remote_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
- if group == "group2":
+ if group == "group1":
for day_shifts in zipped_lists:
assert day_shifts.count("night") >= 2
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
def test_constraint_require_remote_site_presence_week3(self):
- self.Rota.shifts = []
- self.Rota.add_shifts(
+ Rota = generate_limit_constraint_rota()
+ Rota.shifts = []
+ Rota.add_shifts(
SingleShift(
- ("group1", "group2", "group3"),
- "night",
- 12.5,
- days,
+ sites=("group1", "group2", "group3"),
+ name="night",
+ length=12.5,
+ days=days,
balance_offset=20,
workers_required=3,
force_as_block=True,
- constraint=[("require_remote_site_presence_week", ("group2", 1))],
+ constraint=[
+ {
+ "name": "require_remote_site_presence_week",
+ "options": ("group2", 1),
+ }
+ ],
),
)
- self.Rota.build_and_solve(options={"ratio": 0.00})
- self.Rota.export_rota_to_html("remote3")
+ Rota.build_and_solve(options={"ratio": 0.00})
+ Rota.export_rota_to_html("remote3")
- group_workers = self.Rota.get_workers_by_remote_group()
+ group_workers = Rota.get_workers_by_remote_group()
for group in group_workers:
shift_patterns = []
for w in group_workers[group]:
- shift_patterns.append(self.Rota.get_worker_shift_list(w))
+ shift_patterns.append(Rota.get_worker_shift_list(w))
zipped_lists = list(zip(*shift_patterns))
@@ -948,8 +989,9 @@ class TestLimitConstraints:
for day_shifts in zipped_lists:
assert day_shifts.count("night") >= 1
- assert self.Rota.results.solver.status == "ok"
- assert self.Rota.results.solver.termination_condition == "optimal"
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
+
# class TestNoWorkerRota:
# weeks_to_rota = 10
@@ -979,23 +1021,41 @@ class TestNightUnavailable:
# Add a few workers
worker1 = Worker(
- name="worker1", site="group1", grade=1, not_available_to_work=[{ "date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(), "reason" : "****" },]
+ name="worker1",
+ site="group1",
+ grade=1,
+ not_available_to_work=[
+ {
+ "date": datetime.datetime.strptime("15/03/22", "%d/%m/%y").date(),
+ "reason": "****",
+ },
+ ],
)
worker2 = Worker(
- name="worker2", site="group1", grade=1, not_available_to_work=[ { "date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(), "reason": "****" } ]
+ name="worker2",
+ site="group1",
+ grade=1,
+ not_available_to_work=[
+ {
+ "date": datetime.datetime.strptime("14/03/22", "%d/%m/%y").date(),
+ "reason": "****",
+ }
+ ],
)
Rota.add_workers((worker1, worker2))
-
-
def test_basic_assignment(self, capsys):
self.Rota.shifts = []
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",), "night_weekend", 12.5, days[5:], constraint=["night"]
+ sites =("group1",),
+ name = "night_weekend",
+ length = 12.5,
+ days = days[5:],
+ constraint=[{"name": "night"}],
),
)
@@ -1005,7 +1065,6 @@ class TestNightUnavailable:
assert self.Rota.results.solver.status == "ok"
assert self.Rota.results.solver.termination_condition == "optimal"
-
self.Rota.export_rota_to_html("testnight")
summary = self.Rota.get_shift_summary_dict()
@@ -1018,7 +1077,12 @@ class TestNightUnavailable:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",), "night_weekend", 12.5, days[5:], workers_required=2, constraint=["night"]
+ sites =("group1",),
+ name = "night_weekend",
+ length = 12.5,
+ days = days[5:],
+ workers_required=2,
+ constraint=[{"name": "night"}],
),
)
@@ -1032,7 +1096,12 @@ class TestNightUnavailable:
self.Rota.add_shifts(
SingleShift(
- ("group1",), "night_weekend", 12.5, days[5:], workers_required=2, constraint=[]
+ sites = ("group1",),
+ name = "night_weekend",
+ length = 12.5,
+ days = days[5:],
+ workers_required=2,
+ constraint=[],
),
)
@@ -1044,16 +1113,31 @@ class TestNightUnavailable:
def test_assign_split(self):
self.Rota.shifts = []
worker3 = Worker(
- name="worker3", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},)
+ name="worker3",
+ site="group1",
+ grade=1,
+ not_available_to_work=(
+ {"date": datetime.datetime.strptime("13/03/22", "%d/%m/%y").date()},
+ ),
)
worker4 = Worker(
- name="worker4", site="group1", grade=1, not_available_to_work=({"date":datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},)
+ name="worker4",
+ site="group1",
+ grade=1,
+ not_available_to_work=(
+ {"date": datetime.datetime.strptime("12/03/22", "%d/%m/%y").date()},
+ ),
)
self.Rota.add_workers((worker3, worker4))
self.Rota.add_shifts(
SingleShift(
- ("group1",), "night_weekend", 12.5, days[5:], workers_required=3, constraint=[]
+ sites=("group1",),
+ name="night_weekend",
+ length=12.5,
+ days=days[5:],
+ workers_required=3,
+ constraint=[],
),
)
@@ -1067,6 +1151,7 @@ class TestNightUnavailable:
for worker in summary:
assert summary[worker]["night_weekend"] == 15
+
# def test_assign_split_night_constraint(self):
# self.Rota.shifts = []
# worker3 = Worker(
diff --git a/test/test_shifts.py b/test/test_shifts.py
index 1159e9c..e0d123c 100644
--- a/test/test_shifts.py
+++ b/test/test_shifts.py
@@ -49,10 +49,7 @@ class TestShifts:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
),
)
@@ -63,12 +60,116 @@ class TestShifts:
Rota.add_shifts(
SingleShift(
- ("group1"),
- "a",
- 12.5,
- days,
+ sites=("group1",), name="a", length= 12.5, days=days,
),
)
with pytest.raises(InvalidShift):
- Rota.build_and_solve(options={"ratio": 0.000})
\ No newline at end of file
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+
+class TestShiftConstraints:
+
+ def test_max_shifts(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week", "options": 4}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
+
+ def test_max_shifts_fail(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week", "options": 3}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+ assert Rota.results.solver.termination_condition == "infeasible"
+
+ Rota.add_worker(
+ Worker(
+ name="worker3", site="group1", grade=1,
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
+
+
+ def test_max_shifts_per_week_block(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2}}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("max_shifts_per_week_block")
+
+ assert Rota.results.solver.status == "ok"
+ assert Rota.results.solver.termination_condition == "optimal"
+
+
+
+ def test_max_shifts_per_week_block_fail(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 3}}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("max_shifts_per_week_block")
+
+ assert Rota.results.solver.status == "warning"
+
+ def test_max_shifts_per_week_block_shift_number(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2, "shift_number": 7}}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("max_shifts_per_week_block")
+
+ assert Rota.results.solver.status == "ok"
+
+ def test_max_shifts_per_week_block_shift_number_fail(self):
+ Rota = generate_basic_rota()
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ constraint=[{"name": "max_shifts_per_week_block", "options": {"weeks": 2, "shift_number": 6}}],
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("max_shifts_per_week_block")
+
+ assert Rota.results.solver.status == "warning"
\ No newline at end of file
diff --git a/test/test_weekend_padding.py b/test/test_weekend_padding.py
index 7108236..9235c8b 100644
--- a/test/test_weekend_padding.py
+++ b/test/test_weekend_padding.py
@@ -33,18 +33,12 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "a",
- 12.5,
- days,
+ sites=("group1",), name="a", length= 12.5, days=days,
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "b",
- 12.5,
- days[5:],
+ sites=("group1",), name="b", length= 12.5, days=days[5:],
balance_offset=0,
workers_required=1
),
@@ -97,19 +91,13 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "a",
- 12.5,
- days,
+ sites=("group1",), name="a", length= 12.5, days=days,
balance_offset=5,
workers_required=1,
force_as_block=True
),
SingleShift(
- ("group1",),
- "b",
- 12.5,
- days[5:],
+ sites=("group1",), name="b", length= 12.5, days=days[5:],
balance_offset=0,
workers_required=2
),
@@ -159,18 +147,12 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=1
),
)
@@ -211,26 +193,17 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=10,
workers_required=1,
),
SingleShift(
- ("group1",),
- "c",
- 12.5,
- days[1],
+ sites=("group1",), name="c", length= 12.5, days=days[1],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=1
),
)
@@ -259,26 +232,17 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "c",
- 12.5,
- days[1],
+ sites=("group1",), name="c", length= 12.5, days=days[1],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=1
),
)
@@ -318,26 +282,17 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "c",
- 12.5,
- days[0],
+ sites=("group1",), name="c", length= 12.5, days=days[0],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=1
),
)
@@ -368,26 +323,17 @@ class TestDemoRota:
# Add a weekday and weekend shift
self.Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",), name="d", length= 12.5, days=days[:5],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "c",
- 12.5,
- days[0],
+ sites=("group1",), name="c", length= 12.5, days=days[0],
balance_offset=1,
workers_required=1,
),
SingleShift(
- ("group1",),
- "w",
- 12.5,
- days[5:],
+ sites=("group1",), name="w", length= 12.5, days=days[5:],
workers_required=1,
force_as_block=True
),
diff --git a/test/test_worker_requests.py b/test/test_worker_requests.py
index 04ddeca..3e54e73 100644
--- a/test/test_worker_requests.py
+++ b/test/test_worker_requests.py
@@ -50,10 +50,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
force_as_block=False,
),
@@ -90,10 +87,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
force_as_block=False,
),
@@ -128,10 +122,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
force_as_block=False,
),
@@ -173,10 +164,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
force_as_block=False,
),
@@ -218,10 +206,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
balance_offset=1,
force_as_block=False,
@@ -255,10 +240,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
balance_offset=1,
force_as_block=False,
@@ -312,10 +294,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=3,
balance_offset=1,
force_as_block=False,
@@ -356,10 +335,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=2,
balance_offset=1,
force_as_block=False,
@@ -401,10 +377,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "a",
- 12.5,
- days,
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
workers_required=1,
balance_offset=1,
force_as_block=False,
@@ -417,10 +390,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "c",
- 12.5,
- days,
+ sites=("group1", "group2"), name="c", length= 12.5, days=days,
workers_required=1,
balance_offset=1,
force_as_block=False,
@@ -432,10 +402,7 @@ class TestWorkerRequests:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "b",
- 12.5,
- days,
+ sites=("group1", "group2"), name="b", length= 12.5, days=days,
workers_required=1,
balance_offset=1,
force_as_block=False,
@@ -460,4 +427,105 @@ class TestWorkerRequests:
)
with pytest.raises(InvalidShift):
- Rota.build_and_solve(options={"ratio": 0.000})
\ No newline at end of file
+ Rota.build_and_solve(options={"ratio": 0.000})
+
+ def test_work_requests_generic(self):
+ """_summary_"""
+ Rota = generate_basic_rota()
+ Rota.constraint_options["balance_bank_holidays"] = False
+
+ Rota.add_workers(
+ [Worker(
+ name="worker3", site="group1", grade=1,
+ work_requests=(
+ [
+ {"date": i, "shift": "*"}
+ for i in list(date_generator(datetime.date(2022, 3, 7), 35))
+ ]
+ ),
+ ),
+ Worker(
+ name="worker4", site="group1", grade=1,
+ work_requests=(
+ [
+ {"date": i, "shift": "*"}
+ for i in list(date_generator(datetime.date(2022, 3, 7), 35))
+ ]
+ ),
+ ),]
+ )
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days,
+ workers_required=2,
+ balance_offset=1,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.000})
+ Rota.export_rota_to_html("work_requests")
+
+ shift_summary = Rota.get_shift_summary_dict()
+ for worker_name in shift_summary:
+ worker_shifts = shift_summary[worker_name]
+
+ worker = Rota.get_worker_by_name(worker_name)
+
+ assert worker_shifts["a"] == 35
+
+ shift_string = Rota.get_worker_shift_list_string(worker)
+
+ if worker.name in ("worker3", "worker4"):
+ assert shift_string.startswith("a"*35)
+
+ def test_work_requests_generic2(self):
+ """_summary_"""
+ Rota = generate_basic_rota()
+ Rota.constraint_options["balance_bank_holidays"] = False
+
+ Rota.add_workers(
+ [Worker(
+ name="worker3", site="group1", grade=1,
+ work_requests=(
+ [
+ {"date": i, "shift": "*"}
+ for i in list(date_generator(datetime.date(2022, 3, 7), 20))
+ ]
+ ),
+ ),
+ ]
+ )
+
+ Rota.add_shifts(
+ SingleShift(
+ sites=("group1", "group2"), name="a", length= 12.5, days=days[3:],
+ workers_required=2,
+ balance_offset=1,
+ force_as_block=False,
+ ),
+ SingleShift(
+ sites=("group1", "group2"), name="b", length= 12.5, days=days[:3],
+ workers_required=1,
+ balance_offset=1,
+ force_as_block=False,
+ ),
+ )
+
+ Rota.build_and_solve(options={"ratio": 0.1})
+ Rota.export_rota_to_html("work_requests")
+
+ shift_summary = Rota.get_shift_summary_dict()
+ for worker_name in shift_summary:
+ worker_shifts = shift_summary[worker_name]
+
+ worker = Rota.get_worker_by_name(worker_name)
+
+ assert worker_shifts["a"] in (26, 27)
+ assert worker_shifts["b"] == 10
+
+ shift_string = Rota.get_worker_shift_list_string(worker)
+
+ if worker.name in ("worker3",):
+ assert "-" not in shift_string[:20]
\ No newline at end of file
diff --git a/test/test_workers.py b/test/test_workers.py
index 72e9c54..d69ca36 100644
--- a/test/test_workers.py
+++ b/test/test_workers.py
@@ -22,14 +22,20 @@ class TestWorkers:
Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",),
+ name="d",
+ length=12.5,
+ days=days[:5],
balance_offset=10,
workers_required=1,
),
- SingleShift(("group1",), "w", 12.5, days[5:], workers_required=1),
+ SingleShift(
+ sites=("group1",),
+ name="w",
+ length=12.5,
+ days=days[5:],
+ workers_required=1,
+ ),
)
with pytest.raises(NoWorkers):
@@ -49,10 +55,10 @@ class TestWorkers:
Rota.add_shifts(
SingleShift(
- ("group1",),
- "d",
- 12.5,
- days[:5],
+ sites=("group1",),
+ name="d",
+ length=12.5,
+ days=days[:5],
balance_offset=10,
workers_required=1,
),
@@ -128,10 +134,10 @@ class TestWorkers:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "d",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="d",
+ length=12.5,
+ days=days[:5],
balance_offset=10,
workers_required=1,
),
@@ -187,43 +193,44 @@ class TestWorkers:
)
worker1_oop = [
- {
+ {
"start_date": start_date,
"end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
"reason": "First half OOP",
- }
+ }
]
worker2_oop = [
- {
+ {
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
"end_date": Rota.rota_end_date,
"reason": "First half OOP",
- }
+ }
]
worker3_start_date = start_date + datetime.timedelta(weeks=weeks_to_rota / 4)
worker3_oop = [
- {
+ {
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
- "end_date": start_date + datetime.timedelta(weeks=3 * weeks_to_rota / 4),
+ "end_date": start_date
+ + datetime.timedelta(weeks=3 * weeks_to_rota / 4),
"reason": "Quarter oop",
- }
+ }
]
worker4_oop = [
- {
+ {
"start_date": start_date,
"end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 4),
"reason": "Quarter oop",
- },
- {
+ },
+ {
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
"end_date": start_date
+ datetime.timedelta(weeks=weeks_to_rota / 2)
+ datetime.timedelta(weeks=weeks_to_rota / 4),
"reason": "Quarter oop 2",
- },
+ },
]
workers = []
@@ -249,26 +256,26 @@ class TestWorkers:
)
worker6_oop = [
- {
+ {
"start_date": start_date,
"end_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 6),
"reason": "oop",
- },
- {
+ },
+ {
"start_date": start_date + datetime.timedelta(weeks=weeks_to_rota / 2),
"end_date": start_date
+ datetime.timedelta(weeks=weeks_to_rota / 2)
+ datetime.timedelta(weeks=weeks_to_rota / 3),
"reason": " oop 2",
- },
+ },
]
worker7_oop = [
- {
+ {
"start_date": start_date - datetime.timedelta(weeks=1),
"end_date": Rota.rota_end_date,
"reason": "oop",
- },
+ },
]
workers.append(
@@ -282,10 +289,10 @@ class TestWorkers:
Rota.add_shifts(
SingleShift(
- ("group1", "group2"),
- "d",
- 12.5,
- days[:5],
+ sites=("group1", "group2"),
+ name="d",
+ length=12.5,
+ days=days[:5],
balance_offset=10,
workers_required=1,
),
diff --git a/web/__init__.py b/web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/main.py b/web/main.py
new file mode 100644
index 0000000..cdc070e
--- /dev/null
+++ b/web/main.py
@@ -0,0 +1,63 @@
+import uvicorn
+from fastapi import FastAPI, Query
+from pydantic import BaseModel
+
+from fastapi_sqlalchemy import DBSessionMiddleware, db
+
+import os
+from dotenv import load_dotenv
+
+from rota.shifts import SingleShift, ShiftConstraint
+
+from web.models import SingleShift as ModelSingleShift
+from web.models import ShiftConstraint as ModelShiftConstraint
+
+#from rota.wokers
+
+
+load_dotenv('../.env')
+
+app = FastAPI()
+
+app.add_middleware(DBSessionMiddleware, db_url="sqlite:///test.db")
+
+@app.post('/shifts/', response_model=SingleShift)
+async def shifts(shift: ModelSingleShift):
+ db_shift = ModelSingleShift(name=shift.name, length=shift.length)
+ db.session.add(db_shift)
+ db.session.commit()
+ return db_shift
+
+
+@app.get("/shifts/")
+async def shifts():
+ shifts = db.session.query(ModelSingleShift).all()
+
+ return shifts
+
+
+@app.get("/")
+async def root():
+ return {"message": "Hello World"}
+
+@app.get("/files/{file_path:path}")
+async def read_file(file_path: str):
+ return {"file_path": file_path}
+
+
+
+class Item(BaseModel):
+ name: str
+ description: str | None = None
+ price: float
+ tax: float | None = None
+
+
+
+@app.post("/items/")
+async def create_item(item: Item):
+ item.name + item.price
+ return item
+
+if __name__ == '__main__':
+ uvicorn.run(app, host='0.0.0.0', port=8008)
\ No newline at end of file
diff --git a/web/models.py b/web/models.py
new file mode 100644
index 0000000..56f25df
--- /dev/null
+++ b/web/models.py
@@ -0,0 +1,31 @@
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Float, Boolean
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+
+Base = declarative_base()
+
+class SingleShift(Base):
+ __tablename__ = 'single_shift'
+ id = Column(Integer, primary_key=True, index=True)
+ name = Column(String)
+ length = Column(Float)
+ balance_offset = Column(Float, nullable=True)
+ balance_weighting = Column(Float)
+ workers_required = Column(Float)
+ assign_as_block = Column(Boolean)
+ force_as_block = Column(Boolean)
+ hard_constrain_shift = Column(Boolean)
+ bank_holidays_only = Column(Boolean)
+ constraint_id = Column(Integer, ForeignKey('shift_constraint.id'))
+
+ constraint = relationship('ShiftConstraint')
+
+
+class ShiftConstraint(Base):
+ __tablename__ = 'shift_constraint'
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+ options = Column(Integer, nullable=True)
+
+
diff --git a/web/test.py b/web/test.py
new file mode 100644
index 0000000..6e862c2
--- /dev/null
+++ b/web/test.py
@@ -0,0 +1,54 @@
+from datetime import datetime, time, timedelta
+from uuid import UUID
+
+from fastapi import Body, FastAPI,Form, File, UploadFile
+from fastapi.responses import HTMLResponse
+
+app = FastAPI()
+
+
+@app.put("/items/{item_id}")
+async def read_items(
+ item_id: UUID,
+ start_datetime: datetime | None = Body(default=None),
+ end_datetime: datetime | None = Body(default=None),
+ repeat_at: time | None = Body(default=None),
+ process_after: timedelta | None = Body(default=None),
+):
+ start_process = start_datetime + process_after
+ duration = end_datetime - start_process
+ return {
+ "item_id": item_id,
+ "start_datetime": start_datetime,
+ "end_datetime": end_datetime,
+ "repeat_at": repeat_at,
+ "process_after": process_after,
+ "start_process": start_process,
+ "duration": duration,
+ }
+
+@app.post("/files/")
+async def create_files(files: list[bytes] = File()):
+ return {"file_sizes": [len(file) for file in files]}
+
+
+@app.post("/uploadfiles/")
+async def create_upload_files(files: list[UploadFile]):
+ return {"filenames": [file.filename for file in files]}
+
+
+@app.get("/")
+async def main():
+ content = """
+
+
+
+
+ """
+ return HTMLResponse(content=content)
\ No newline at end of file