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 %} + + {% endfor %} + + + + + {% for user, bookings in bookings_by_user.items %} + + + {% for _ in hours %} + + {% endfor %} + + {% endfor %} + + +
{{ hour|date:"TIME_FORMAT" }}
{{ bookings.0.get_user_block }} + {% if forloop.first %} + {% for booking in bookings %} +
{{ booking.start_time }} - {{ booking.end_time }}
+ {% endfor %} + {% endif %} +
+ {% 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