Skip to content

Commit

Permalink
fix slot overlap and add vacation
Browse files Browse the repository at this point in the history
  • Loading branch information
mamico committed Aug 10, 2023
1 parent 36a0d6b commit 4b7c3e6
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 49 deletions.
45 changes: 21 additions & 24 deletions src/redturtle/prenotazioni/adapters/booker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,20 @@
from plone import api
from plone.memoize.instance import memoize
from random import choice
from redturtle.prenotazioni import _
from redturtle.prenotazioni import datetime_with_tz
from redturtle.prenotazioni import logger
from redturtle.prenotazioni.adapters.slot import BaseSlot
from redturtle.prenotazioni.config import VERIFIED_BOOKING
from zope.annotation.interfaces import IAnnotations
from zope.component import Interface
from zope.interface import implementer
from redturtle.prenotazioni import _
from redturtle.prenotazioni.content.prenotazione import VACATION_TYPE
from redturtle.prenotazioni.prenotazione_event import MovedPrenotazione
from redturtle.prenotazioni.utilities.dateutils import exceedes_date_limit
from redturtle.prenotazioni.content.prenotazione import VACATION_TYPE
from zope.event import notify
from redturtle.prenotazioni import datetime_with_tz
from six.moves.urllib.parse import parse_qs
from six.moves.urllib.parse import urlparse
from redturtle.prenotazioni.adapters.slot import BaseSlot
from DateTime import DateTime
from zope.annotation.interfaces import IAnnotations
from zope.component import Interface
from zope.event import notify
from zope.interface import implementer


class BookerException(Exception):
Expand Down Expand Up @@ -230,7 +229,11 @@ def create_vacation(self, data):
gate_busy_slots = busy_slots.get(gate, [])
if gate_busy_slots:
for slot in gate_busy_slots:
if vacation_slot.intersect(slot):
intersection = vacation_slot.intersect(slot)
if (
intersection
and intersection.lower_value != intersection.upper_value
):
has_slot_conflicts = True
break

Expand All @@ -244,22 +247,16 @@ def create_vacation(self, data):
msg = self.context.translate(_("This day is not valid."))
raise BookerException(msg)

slots = []
for period in ("morning", "afternoon"):
free_slots = self.prenotazioni.get_free_slots(start, period)
gate_free_slots = free_slots.get(gate, [])
for slot in gate_free_slots:
if vacation_slot.overlaps(slot):
slots.append(vacation_slot.intersect(slot))

start_date = DateTime(start.strftime("%Y/%m/%d"))
for slot in slots:
booking_date = start_date + (float(slot.lower_value) / 86400)
slot.__class__ = BaseSlot
duration = float(len(slot)) / 86400
# duration = float(len(slot)) / 60
slot_data = {k: v for k, v in data.items() if k != "gate"}
slot_data["booking_date"] = datetime_with_tz(booking_date)
slot_data["booking_type"] = VACATION_TYPE
self.create(slot_data, duration=duration, force_gate=gate)
return len(slots)
# there is a slot that overlaps with the vacation
duration = (end - start).seconds / 24 / 60 / 60
# XXX: weird to remove the gate from data and then force it ...
slot_data = {k: v for k, v in data.items() if k != "gate"}
slot_data["booking_date"] = start
slot_data["booking_type"] = VACATION_TYPE
if self.create(slot_data, duration=duration, force_gate=gate):
return 1
2 changes: 1 addition & 1 deletion src/redturtle/prenotazioni/adapters/slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class BaseSlot(Interval):
extra_css_styles = []

def __repr__(self):
return f"[{self.start()}:{self.end()}]"
return f"[{self.start()}:{self.stop()}]"

@staticmethod
def time2seconds(value):
Expand Down
37 changes: 28 additions & 9 deletions src/redturtle/prenotazioni/browser/prenotazioni_context_state.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from datetime import date
from datetime import datetime

from datetime import timedelta
# TODO: togliere DateTime ?
from DateTime import DateTime
from datetime import timedelta
from plone import api
from plone.memoize.view import memoize
from Products.Five.browser import BrowserView
Expand All @@ -18,13 +17,13 @@
from redturtle.prenotazioni.config import PAUSE_PORTAL_TYPE
from redturtle.prenotazioni.config import PAUSE_SLOT
from redturtle.prenotazioni.content.pause import Pause
from redturtle.prenotazioni.utilities.urls import urlify
from redturtle.prenotazioni.utilities.dateutils import hm2DT
from redturtle.prenotazioni.utilities.dateutils import hm2seconds

from redturtle.prenotazioni.utilities.urls import urlify
from six.moves import map
from six.moves import range

import itertools
import json
import six

Expand Down Expand Up @@ -302,7 +301,24 @@ def get_booking_urls(self, day, slot, slot_min_size=0, gate=None):
def get_all_booking_urls_by_gate(self, day, slot_min_size=0):
"""Get all the booking urls divided by gate
XXX: used only by 'get_all_booking_urls' !!!
slot_min_size: seconds
Return a dict like {
gate: [
{
'title': '08:00',
'url': 'http://.../prenotazione_add?form.booking_date=2023-08-09T08%3A00%3A00%2B02%3A00',
'class': 'oclock',
'booking_date': datetime.datetime(2023, 8, 9, 8, 0, tzinfo=<DstTzInfo 'Europe/Rome' CEST+2:00:00 DST>),
'future': False
},
{
'title': '08:05',
...
]
}
"""
slots_by_gate = self.get_free_slots(day)
urls = {}
Expand All @@ -324,10 +340,9 @@ def get_all_booking_urls(self, day, slot_min_size=0):
"""
urls_by_gate = self.get_all_booking_urls_by_gate(day, slot_min_size)
urls = {}
for gate in urls_by_gate:
for url in urls_by_gate[gate]:
urls[url["title"]] = url
return sorted(six.itervalues(urls), key=lambda x: x["title"])
for url in itertools.chain.from_iterable(urls_by_gate.values()):
urls[url["title"]] = url
return sorted(urls.values(), key=lambda x: x["title"])

def is_slot_busy(self, day, slot):
"""Check if a slot is busy (i.e. the is no free slot overlapping it)"""
Expand All @@ -344,6 +359,9 @@ def is_slot_busy(self, day, slot):
def get_anonymous_booking_url(self, day, slot, slot_min_size=0):
"""Returns, the the booking url for an anonymous user
Returns the first available/bookable slot/url that fits
the slot boundaries
slot_min_size: seconds
"""
# First we check if we have booking urls
Expand All @@ -355,6 +373,7 @@ def get_anonymous_booking_url(self, day, slot, slot_min_size=0):
else:
return self.unavailable_slot_booking_url
# Otherwise we check if the URL fits the slot boundaries
# HH:MM in localtime
slot_start = slot.start()
slot_stop = slot.stop()

Expand Down Expand Up @@ -675,7 +694,7 @@ def get_busy_slots(self, booking_date, period="day"):
slots_by_gate.setdefault(slot.gate, []).append(slot)
return slots_by_gate

# @memoize
@memoize
def get_free_slots(self, booking_date, period="day"):
"""This will return the free slots divided by gate
Expand Down
3 changes: 2 additions & 1 deletion src/redturtle/prenotazioni/browser/vacations.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ def has_slot_conflicts(self, data):
return False
vacation_slot = self.get_vacation_slot(data)
for slot in gate_busy_slots:
if vacation_slot.intersect(slot):
intersection = vacation_slot.intersect(slot)
if intersection and intersection.lower_value != intersection.upper_value:
return True
return False

Expand Down
19 changes: 11 additions & 8 deletions src/redturtle/prenotazioni/tests/test_available_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from plone.app.testing import TEST_USER_ID
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.testing import RelativeSession
from redturtle.prenotazioni import datetime_with_tz
from redturtle.prenotazioni.adapters.booker import IBooker
from redturtle.prenotazioni.testing import REDTURTLE_PRENOTAZIONI_API_FUNCTIONAL_TESTING

Expand All @@ -24,10 +23,11 @@ class TestAvailableSlots(unittest.TestCase):
maxDiff = None
timezone = "Europe/Rome"

def dt_local_to_utc(self, value):
return pytz.timezone(self.timezone).localize(value).astimezone(pytz.utc)

def dt_local_to_json(self, value):
return json_compatible(
pytz.timezone(self.timezone).localize(value).astimezone(pytz.utc)
)
return json_compatible(self.dt_local_to_utc(value))

def setUp(self):
self.app = self.layer["app"]
Expand Down Expand Up @@ -125,7 +125,9 @@ def test_month_slots_called_without_params_return_available_slots_of_current_mon
booker = IBooker(self.folder_prenotazioni)
booker.create(
{
"booking_date": datetime(current_year, current_month, monday, 7, 0),
"booking_date": self.dt_local_to_utc(
datetime(current_year, current_month, monday, 7, 0)
),
"booking_type": "Type A",
"title": "foo",
}
Expand All @@ -144,11 +146,11 @@ def test_month_slots_called_without_params_return_available_slots_of_current_mon
)
)

# first free slot is at 7:30 of the next month
# first free slot is at 7:30 (localtime) of the next month
self.assertEqual(
response.json()["items"][0],
json_compatible(
datetime_with_tz(datetime(current_year, next_month, monday, 7, 30))
self.dt_local_to_json(
datetime(current_year, next_month, monday, 7, 30)
),
)
else:
Expand Down Expand Up @@ -238,6 +240,7 @@ def test_if_start_and_end_return_all_available_slots_between_these_dates(
datetime(current_year, next_month, monday, 9, 0)
)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(expected, response.json()["items"])

def test_raise_error_if_start_is_greater_than_end(
Expand Down
14 changes: 8 additions & 6 deletions src/redturtle/prenotazioni/tests/test_vacation_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def setUp(self):
row["morning_end"] = "1200"
self.folder_prenotazioni.week_table = week_table

self.next_monday = datetime.now().astimezone(pytz.utc) + timedelta(
days=(datetime.today().weekday()) % 7 + 7
)
self.next_monday = datetime.now().replace(second=0, microsecond=0).astimezone(
pytz.utc
) + timedelta(days=(datetime.today().weekday()) % 7 + 7)

api.content.transition(obj=self.folder_prenotazioni, transition="publish")

Expand All @@ -69,8 +69,9 @@ def tearDown(self):
self.api_session_anon.close()

def test_add_vacation(self):
start = self.next_monday.replace(hour=10, minute=0)
end = self.next_monday.replace(hour=11, minute=30)
# UTC time
start = self.next_monday.replace(hour=8, minute=0)
end = self.next_monday.replace(hour=9, minute=30)
gate = self.folder_prenotazioni.gates[0]
res = self.api_session_admin.post(
f"{self.folder_prenotazioni.absolute_url()}/@vacation",
Expand All @@ -86,7 +87,7 @@ def test_add_vacation(self):
res = self.api_session_anon.post(
self.folder_prenotazioni.absolute_url() + "/@booking",
json={
"booking_date": self.next_monday.replace(hour=10, minute=0).isoformat(),
"booking_date": start.isoformat(),
"booking_type": "Type A",
"fields": [
{"name": "title", "value": "Mario Rossi"},
Expand Down Expand Up @@ -123,6 +124,7 @@ def test_add_vacation(self):

def test_add_vacation_wrong_hours(self):
# add vacation outside of working hours
# UTC time
start = self.next_monday.replace(hour=20, minute=0)
end = self.next_monday.replace(hour=21, minute=30)
gate = self.folder_prenotazioni.gates[0]
Expand Down

0 comments on commit 4b7c3e6

Please sign in to comment.