manager: add partial bookings month view (#79654)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-07-12 18:01:36 +02:00
parent 0cc06d2047
commit d1597d7ab3
6 changed files with 230 additions and 13 deletions

View File

@ -131,7 +131,9 @@ class AgendaEditForm(forms.ModelForm):
if not EventsType.objects.exists():
del self.fields['events_type']
if kwargs['instance'].partial_bookings:
del self.fields['default_view']
self.fields['default_view'].choices = [
(k, v) for k, v in self.fields['default_view'].choices if k not in ('open_events', 'week')
]
class AgendaBookingDelaysForm(forms.ModelForm):

View File

@ -697,6 +697,21 @@ div#appbar a.active {
}
}
.agenda-table.partial-bookings .booking {
height: 70%;
width: 100%;
position: absolute;
right: 0;
top: 15%;
background: #1066bc;
&.present {
background: hsl(120, 57%, 35%);
}
&.absent {
background: hsl(355, 80%, 45%);
}
}
/* ants-hub */
ul.objects-list.single-links li.ants-setting-not-configured a.edit {
color: red;

View File

@ -3,16 +3,16 @@
{% now "m" as today_month %}
{% now "j" as today_day %}
{% now "Ymj" as today %}
{% 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>
{% if not no_opened and agenda.kind == 'events' and not agenda.partial_bookings %}
<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>
{% if not agenda.partial_bookings %}
<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>
{% endif %}
<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,35 @@
{% extends "chrono/manager_agenda_month_view.html" %}
{% load i18n %}
{% block content %}
<table class="agenda-table partial-bookings">
<thead>
<tr>
<td></td>
{% for day in days %}
<th>
<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day|date:"d" }}</a>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for booking_info in user_booking_info %}
<tr class="{% cycle 'odd' 'even' %}">
<th>{{ booking_info.user_name }}</th>
{% for booking in booking_info.bookings %}
<td class="day-cell">
{% if booking %}
<span class="booking {{ booking.check_css_class }}"></span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -22,7 +22,7 @@ import itertools
import json
import math
import uuid
from operator import attrgetter
from operator import attrgetter, itemgetter
import requests
from dateutil.relativedelta import MO, relativedelta
@ -1673,6 +1673,7 @@ class AgendaWeekMonthMixin:
def get_dated_items(self):
date_list, object_list, extra_context = super().get_dated_items()
if self.agenda.kind == 'events':
self.events = object_list
min_start = self.first_day
max_start = getattr(self, 'get_next_%s' % self.kind)(self.first_day)
exceptions = TimePeriodException.objects.filter(
@ -1690,6 +1691,8 @@ class AgendaWeekMonthMixin:
).all()
else:
context['single_desk'] = bool(len(self.agenda.prefetched_desks) == 1)
if self.agenda.partial_bookings:
self.fill_partial_bookings_context(context)
return context
def get_timeperiods(self):
@ -1864,6 +1867,49 @@ class AgendaWeekMonthMixin:
return timetable
def fill_partial_bookings_context(self, context):
first_day_next_month = self.get_next_month(self.first_day)
context['days'] = days = [
self.first_day + datetime.timedelta(days=i)
for i in range((first_day_next_month - self.first_day).days)
]
booking_info_by_user = {}
bookings = Booking.objects.filter(event__in=self.events)
for booking in bookings:
booking_info = booking_info_by_user.setdefault(
booking.user_external_id,
{
'user_name': booking.user_name,
'user_first_name': booking.user_first_name,
'user_last_name': booking.user_last_name,
'bookings': [None] * len(days),
},
)
user_bookings = booking_info['bookings']
if booking.user_was_present is not None:
booking.check_css_class = 'present' if booking.user_was_present else 'absent'
user_bookings[localtime(booking.event.start_datetime).day - 1] = booking
subscriptions = self.agenda.subscriptions.filter(
date_start__lt=first_day_next_month,
date_end__gte=self.first_day,
).exclude(user_external_id__in=booking_info_by_user.keys())
for subscription in subscriptions:
booking_info_by_user[subscription.user_external_id] = {
'user_name': subscription.user_name,
'user_first_name': subscription.user_first_name,
'user_last_name': subscription.user_last_name,
'bookings': [None] * len(days),
}
context['user_booking_info'] = sorted(
booking_info_by_user.values(), key=itemgetter('user_last_name', 'user_first_name')
)
class AgendaWeekView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView, WeekMixin):
kind = 'week'
@ -1920,6 +1966,8 @@ class AgendaMonthView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView):
def get_template_names(self):
if self.agenda.kind == 'virtual':
return ['chrono/manager_meetings_agenda_month_view.html']
if self.agenda.partial_bookings:
return ['chrono/manager_partial_bookings_month_view.html']
return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
def get_previous_month_url(self):

View File

@ -29,7 +29,11 @@ def test_manager_partial_bookings_add_agenda(app, admin_user, settings):
assert agenda.default_view == 'day'
resp = resp.click('Options')
assert 'default_view' not in resp.form.fields
assert resp.form['default_view'].options == [
('', False, '---------'),
('day', True, 'Day view'),
('month', False, 'Month view'),
]
def test_manager_partial_bookings_add_event(app, admin_user):
@ -103,7 +107,6 @@ def test_manager_partial_bookings_day_view(app, admin_user, freezer):
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('.partial-booking--hour')[0].text == '07\u202fh'
@ -357,3 +360,117 @@ def test_manager_partial_bookings_check_filters(check_types, app, admin_user):
resp = app.get(url, params={'booking-status': 'absence::foo-reason'})
assert [x.text_content() for x in resp.pyquery('.registrant--name')] == ['User Absent Meat Foo Reason']
def test_manager_partial_bookings_month_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
)
event2 = Event.objects.create(
label='Event 2',
start_datetime=start_datetime + datetime.timedelta(days=2),
end_time=datetime.time(18, 00),
places=10,
agenda=agenda,
)
for e in (event, event2):
Booking.objects.create(
user_external_id='user:1',
user_first_name='User',
user_last_name='Not Checked',
start_time=datetime.time(11, 00),
end_time=datetime.time(13, 30),
event=e,
)
Booking.objects.create(
user_external_id='user:2',
user_first_name='User',
user_last_name='Present',
start_time=datetime.time(8, 00),
end_time=datetime.time(10, 00),
user_check_start_time=datetime.time(8, 00),
user_check_end_time=datetime.time(10, 00),
event=event,
user_was_present=True,
)
Booking.objects.create(
user_external_id='user:3',
user_first_name='User',
user_last_name='Absent',
start_time=datetime.time(12, 00),
end_time=datetime.time(14, 00),
user_check_start_time=datetime.time(12, 30),
user_check_end_time=datetime.time(14, 30),
event=event,
user_was_present=False,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:1',
user_first_name='Subscription',
user_last_name='Present',
date_start=event.start_datetime,
date_end=event.start_datetime + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:4',
user_first_name='Subscription',
user_last_name='Not Booked',
date_start=event.start_datetime,
date_end=event.start_datetime + datetime.timedelta(days=1),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:5',
user_first_name='Subscription',
user_last_name='Next Month',
date_start=datetime.date(2023, 6, 1),
date_end=datetime.date(2023, 6, 10),
)
Subscription.objects.create(
agenda=agenda,
user_external_id='user:6',
user_first_name='Subscription',
user_last_name='Previous Month',
date_start=datetime.date(2023, 4, 20),
date_end=datetime.date(2023, 4, 30),
)
app = login(app)
today = start_datetime.date()
resp = app.get('/manage/agendas/%s/month/%d/%d/%d/' % (agenda.pk, today.year, today.month, today.day))
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 32))
assert [x.text for x in resp.pyquery('tbody tr th')] == [
'User Absent',
'Subscription Not Booked',
'User Not Checked',
'User Present',
]
user_absent_row = resp.pyquery('tbody tr')[0]
assert len(resp.pyquery(user_absent_row)('td')) == 31
assert len(resp.pyquery(user_absent_row)('td span')) == 1
assert len(resp.pyquery(user_absent_row)('td span.booking.absent')) == 1
subscription_not_booked_row = resp.pyquery('tbody tr')[1]
assert len(resp.pyquery(subscription_not_booked_row)('td')) == 31
assert len(resp.pyquery(subscription_not_booked_row)('td span')) == 0
user_not_checked_row = resp.pyquery('tbody tr')[2]
assert len(resp.pyquery(user_not_checked_row)('td')) == 31
assert len(resp.pyquery(user_not_checked_row)('td span.booking')) == 2
user_present_row = resp.pyquery('tbody tr')[3]
assert len(resp.pyquery(user_present_row)('td')) == 31
assert len(resp.pyquery(user_present_row)('td span')) == 1
assert len(resp.pyquery(user_present_row)('td span.booking.present')) == 1
resp = resp.click('Next month')
assert [int(x.text) for x in resp.pyquery('thead th a')] == list(range(1, 31))
assert [x.text for x in resp.pyquery('tbody tr th')] == ['Subscription Next Month']
assert len(resp.pyquery('tbody tr td')) == 30