manager: add partial bookings day view (#78056)

This commit is contained in:
Valentin Deniaud 2023-05-31 12:19:45 +02:00
parent 21c0d0da1f
commit 1b9bd2f428
6 changed files with 156 additions and 13 deletions

View File

@ -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):

View File

@ -597,3 +597,10 @@ div#appbar a.active {
background: #386ede;
color: white;
}
table.partial-bookings {
border-spacing: 0;
td.hour-cell {
outline: solid 1px;
}
}

View File

@ -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' %}
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
{% if not agenda.partial_bookings %}
{% if not no_opened and agenda.kind == 'events' %}
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
{% endif %}
<span class="buttons-group">
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
</span>
{% endif %}
<span class="buttons-group">
<a {% if active == 'day' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Day' %}</a>
<a {% if active == 'week' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-week-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Week' %}</a>
<a {% if active == 'month' %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-month-view' pk=agenda.pk year=view.date|date:"Y"|default:today_year|default:today_year month=view.date|date:"m"|default:today_month day=view.date|date:"d"|default:today_day %}">{% trans 'Month' %}</a>
</span>
{% if not no_today %}
<a {% if active == 'day' and view.date|date:"Ymj" == today %}class="active"{% endif%} href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=today_year month=today_month day=today_day %}">{% trans 'Today' %}</a>
{% endif %}

View File

@ -0,0 +1,43 @@
{% extends "chrono/manager_agenda_day_view.html" %}
{% load i18n %}
{% block content %}
{% if not hours %}
<div>
<p>{% trans "No opening hours this day." %}</p>
</div>
{% else %}
<table class="agenda-table partial-bookings">
<thead>
<tr>
<td></td>
{% for hour in hours %}
<th>{{ hour|date:"TIME_FORMAT" }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for user, bookings in bookings_by_user.items %}
<tr class="{% cycle 'odd' 'even' %}">
<th>{{ bookings.0.get_user_block }}</th>
{% for _ in hours %}
<td class="hour-cell">
{% if forloop.first %}
{% for booking in bookings %}
<div class="booking"
style="left: {{ booking.css_left }}%; width: {{ booking.css_width }}%;"
>{{ booking.start_time }} - {{ booking.end_time }}</div>
{% endfor %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -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()

View File

@ -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