diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index f65c3212..8c353558 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -93,6 +93,7 @@ class AgendaAddForm(forms.ModelForm):
if self.cleaned_data.get('kind') == 'partial-bookings':
self.cleaned_data['kind'] = 'events'
self.instance.partial_bookings = True
+ self.instance.default_view = 'day'
def save(self, *args, **kwargs):
create = self.instance.pk is None
@@ -129,6 +130,8 @@ class AgendaEditForm(forms.ModelForm):
else:
if not EventsType.objects.exists():
del self.fields['events_type']
+ if kwargs['instance'].partial_bookings:
+ del self.fields['default_view']
class AgendaBookingDelaysForm(forms.ModelForm):
diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index 6912b292..5e3e2395 100644
--- a/chrono/manager/static/css/style.scss
+++ b/chrono/manager/static/css/style.scss
@@ -597,3 +597,10 @@ div#appbar a.active {
background: #386ede;
color: white;
}
+
+table.partial-bookings {
+ border-spacing: 0;
+ td.hour-cell {
+ outline: solid 1px;
+ }
+}
diff --git a/chrono/manager/templates/chrono/manager_agenda_view_buttons_fragment.html b/chrono/manager/templates/chrono/manager_agenda_view_buttons_fragment.html
index 6228cb10..03523dd1 100644
--- a/chrono/manager/templates/chrono/manager_agenda_view_buttons_fragment.html
+++ b/chrono/manager/templates/chrono/manager_agenda_view_buttons_fragment.html
@@ -3,14 +3,16 @@
{% now "m" as today_month %}
{% now "j" as today_day %}
{% now "Ymj" as today %}
-{% if not no_opened and agenda.kind == 'events' %}
- {% trans 'Open events' %}
+{% if not agenda.partial_bookings %}
+ {% if not no_opened and agenda.kind == 'events' %}
+ {% trans 'Open events' %}
+ {% endif %}
+
+ {% trans 'Day' %}
+ {% trans 'Week' %}
+ {% trans 'Month' %}
+
{% endif %}
-
- {% trans 'Day' %}
- {% trans 'Week' %}
- {% trans 'Month' %}
-
{% if not no_today %}
{% trans 'Today' %}
{% endif %}
diff --git a/chrono/manager/templates/chrono/manager_partial_bookings_day_view.html b/chrono/manager/templates/chrono/manager_partial_bookings_day_view.html
new file mode 100644
index 00000000..ff7a8b35
--- /dev/null
+++ b/chrono/manager/templates/chrono/manager_partial_bookings_day_view.html
@@ -0,0 +1,43 @@
+{% extends "chrono/manager_agenda_day_view.html" %}
+{% load i18n %}
+
+{% block content %}
+
+ {% if not hours %}
+
+
{% trans "No opening hours this day." %}
+
+ {% else %}
+
+
+
+
+ |
+ {% for hour in hours %}
+ {{ hour|date:"TIME_FORMAT" }} |
+ {% endfor %}
+
+
+
+
+ {% for user, bookings in bookings_by_user.items %}
+
+ {{ bookings.0.get_user_block }} |
+ {% for _ in hours %}
+
+ {% if forloop.first %}
+ {% for booking in bookings %}
+ {{ booking.start_time }} - {{ booking.end_time }}
+ {% endfor %}
+ {% endif %}
+ |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+ {% endif %}
+{% endblock %}
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 98fbcd16..5a7e2058 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1370,6 +1370,8 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
def get_template_names(self):
if self.agenda.kind == 'virtual':
return ['chrono/manager_meetings_agenda_day_view.html']
+ if self.agenda.partial_bookings:
+ return ['chrono/manager_partial_bookings_day_view.html']
return ['chrono/manager_%s_agenda_day_view.html' % self.agenda.kind]
def get_previous_day_url(self):
@@ -1487,6 +1489,37 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
current_date += interval
first = False
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ if self.agenda.partial_bookings:
+ self.fill_partial_bookings_context(context)
+ return context
+
+ def fill_partial_bookings_context(self, context):
+ events = self.agenda.event_set.filter(start_datetime__date=self.date.date())
+ if not events.exists():
+ return
+
+ event_times = events.aggregate(Min('start_datetime'), Max('end_time'))
+ min_time = localtime(event_times['start_datetime__min']).time()
+ max_time = event_times['end_time__max']
+
+ start_time = datetime.time(min_time.hour - 1, 0)
+ end_time = datetime.time(max_time.hour + 2, 0)
+ context['hours'] = [datetime.time(hour=i) for i in range(start_time.hour, end_time.hour)]
+
+ def get_time_ratio(t1, t2):
+ return 100 * ((t1.hour - t2.hour) * 60 + t1.minute - t2.minute) // 60
+
+ bookings = Booking.objects.filter(event__in=events)
+ bookings_by_user = collections.defaultdict(list)
+ for booking in bookings:
+ booking.css_left = get_time_ratio(booking.start_time, start_time)
+ booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
+ bookings_by_user[booking.user_external_id].append(booking)
+
+ context['bookings_by_user'] = dict(bookings_by_user)
+
agenda_day_view = AgendaDayView.as_view()
diff --git a/tests/manager/test_partial_bookings.py b/tests/manager/test_partial_bookings.py
index e0aa4795..17591fc4 100644
--- a/tests/manager/test_partial_bookings.py
+++ b/tests/manager/test_partial_bookings.py
@@ -3,6 +3,7 @@ import datetime
import pytest
from chrono.agendas.models import Agenda, Booking, Event
+from chrono.utils.timezone import make_aware
from tests.utils import login
pytestmark = pytest.mark.django_db
@@ -23,6 +24,10 @@ def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
agenda = Agenda.objects.get(label='Foo bar')
assert agenda.kind == 'events'
assert agenda.partial_bookings is True
+ assert agenda.default_view == 'day'
+
+ resp = resp.click('Options')
+ assert 'default_view' not in resp.form.fields
def test_manager_partial_bookings_add_event(app, admin_user):
@@ -47,10 +52,60 @@ def test_manager_partial_bookings_add_event(app, admin_user):
assert 'duration' not in resp.form.fields
assert resp.form['end_time'].value == '18:00'
- resp.form['end_time'] = '08:01'
- resp = resp.form.submit().follow()
- resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.pk, event.pk))
- resp.form['end_time'] = '07:59'
- resp = resp.form.submit()
- assert 'End time must be greater than start time.' in resp.text
+def test_manager_partial_bookings_day_view(app, admin_user, freezer):
+ agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)
+ start_datetime = make_aware(datetime.datetime(2023, 5, 2, 8, 0))
+ event = Event.objects.create(
+ label='Event', start_datetime=start_datetime, end_time=datetime.time(18, 00), places=10, agenda=agenda
+ )
+ Booking.objects.create(
+ user_external_id='xxx',
+ user_first_name='Jane',
+ user_last_name='Doe',
+ start_time=datetime.time(11, 00),
+ end_time=datetime.time(13, 30),
+ event=event,
+ )
+ Booking.objects.create(
+ user_external_id='yyy',
+ user_first_name='Jon',
+ user_last_name='Doe',
+ start_time=datetime.time(8, 00),
+ end_time=datetime.time(10, 00),
+ event=event,
+ )
+ Booking.objects.create(
+ user_external_id='yyy',
+ user_first_name='Jon',
+ user_last_name='Doe',
+ start_time=datetime.time(12, 00),
+ end_time=datetime.time(14, 00),
+ event=event,
+ )
+
+ app = login(app)
+ today = start_datetime.date()
+ resp = app.get('/manage/agendas/%s/day/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
+ assert 'Week' not in resp.text
+ assert 'Month' not in resp.text
+
+ # time range from one hour before event start to one hour after end
+ assert (
+ resp.pyquery('thead th').text()
+ == '7 a.m. 8 a.m. 9 a.m. 10 a.m. 11 a.m. noon 1 p.m. 2 p.m. 3 p.m. 4 p.m. 5 p.m. 6 p.m. 7 p.m.'
+ )
+
+ assert len(resp.pyquery('tbody tr')) == 2
+ assert resp.pyquery('tbody tr th')[0].text == 'Jane Doe'
+ assert resp.pyquery('tbody tr th')[1].text == 'Jon Doe'
+
+ assert resp.pyquery('tbody tr div.booking')[0].text == '11 a.m. - 1:30 p.m.'
+ assert resp.pyquery('tbody tr div.booking')[0].attrib['style'] == 'left: 400%; width: 250%;'
+ assert resp.pyquery('tbody tr div.booking')[1].text == '8 a.m. - 10 a.m.'
+ assert resp.pyquery('tbody tr div.booking')[1].attrib['style'] == 'left: 100%; width: 200%;'
+ assert resp.pyquery('tbody tr div.booking')[2].text == 'noon - 2 p.m.'
+ assert resp.pyquery('tbody tr div.booking')[2].attrib['style'] == 'left: 500%; width: 200%;'
+
+ resp = resp.click('Next day')
+ assert 'No opening hours this day.' in resp.text