manager: add partial bookings month view (#79654)
gitea/chrono/pipeline/head This commit looks good
Details
gitea/chrono/pipeline/head This commit looks good
Details
This commit is contained in:
parent
0cc06d2047
commit
d1597d7ab3
|
@ -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):
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue