manager: check counters (#74489)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Lauréline Guérin 2023-02-16 11:02:40 +01:00
parent 73192ed8bf
commit a6b0ed6abf
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
12 changed files with 182 additions and 77 deletions

View File

@ -37,7 +37,20 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import connection, models, transaction
from django.db.models import Count, Exists, ExpressionWrapper, F, Func, Max, OuterRef, Prefetch, Q, Value
from django.db.models import (
Count,
Exists,
ExpressionWrapper,
F,
Func,
IntegerField,
Max,
OuterRef,
Prefetch,
Q,
Subquery,
Value,
)
from django.db.models.functions import Cast, Coalesce, Concat, ExtractWeek, ExtractWeekDay
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
from django.urls import reverse
@ -1738,6 +1751,26 @@ class Event(models.Model):
return Event.annotate_queryset_with_overlaps(qs, booked_events)
@staticmethod
def annotate_booking_checks(qs):
bookings = (
Booking.objects.filter(
event=OuterRef('pk'), cancellation_datetime__isnull=True, in_waiting_list=False
)
.order_by()
.values('event')
)
present_count = bookings.filter(user_was_present=True).annotate(count=Count('event')).values('count')
absent_count = bookings.filter(user_was_present=False).annotate(count=Count('event')).values('count')
notchecked_count = (
bookings.filter(user_was_present__isnull=True).annotate(count=Count('event')).values('count')
)
return qs.annotate(
present_count=Coalesce(Subquery(present_count, output_field=IntegerField()), Value(0)),
absent_count=Coalesce(Subquery(absent_count, output_field=IntegerField()), Value(0)),
notchecked_count=Coalesce(Subquery(notchecked_count, output_field=IntegerField()), Value(0)),
)
@property
def remaining_places(self):
return max(0, self.places - self.booked_places)

View File

@ -17,10 +17,6 @@
background: #e33;
}
li.not-bookable {
opacity: 0.7;
}
li.full {
background: #f8f8fe;
}
@ -398,10 +394,33 @@ ul.objects-list.single-links li a.link-action-icon.refresh {
}
ul.objects-list.single-links li a.link-action-icon.duplicate {
margin-right: 3em;
&::before {
content: "\f24d"; /* clone */
}
margin-right: 3em;
&::before {
content: "\f24d"; /* clone */
}
}
ul.objects-list.single-links.events-list {
li a:first-child {
line-height: 2;
padding: 1ex 0.5ex 1ex 2ex;
span.event-title {
font-weight: 600;
}
span.tags {
span.tag {
left: 0;
}
}
}
}
div.event-title-meta span.tag {
box-sizing: border-box;
border-radius: 1ex;
padding: 0 1ex;
background: #386ede;
color: white;
}
div.ui-dialog form p span.datetime input {
@ -543,6 +562,10 @@ div.agenda-settings .pk-tabs--container {
}
}
#event_details {
margin: 1em 0;
}
@media print {
#event_details {
a.delete,

View File

@ -9,43 +9,55 @@
data-total="{{ event.waiting_list_places }}" data-booked="{{ event.booked_waiting_list_places }}"
{% endif %}
><a href="{% if view_mode == 'settings_view' %}{% url 'chrono-manager-event-edit' pk=agenda.pk event_pk=event.pk %}?next=settings{% elif event.pk %}{% url 'chrono-manager-event-view' pk=agenda.pk event_pk=event.pk %}{% else %}{% url 'chrono-manager-event-create-recurrence' pk=agenda.pk event_identifier=event.slug %}{% endif %}">
{% if event.cancellation_status %}
<span class="tag">{{ event.cancellation_status }}</span>
{% else %}
{% if event.main_list_full %}
<span class="full tag">{% trans "Full" %}</span>
{% endif %}
{% if event.checked %}
<span class="checked tag">{% trans "Checked" %}</span>
{% endif %}
{% endif %}
<span class="event-info">
{% if view_mode == 'settings_view' %}
{% if event.label %}{{ event.label }} {% endif %}[{% trans "identifier:" %} {{ event.slug }}]
{% if event.label %}<span class="event-title">{{ event.label }} {% endif %}[{% trans "identifier:" %} {{ event.slug }}]</span>
{% else %}
{% if event.label %}{{ event.label }} / {% endif %}
{% endif %}
{% if not event.recurrence_days %}
{% if view_mode == 'day_view' %}{{ event.start_datetime|time }}{% else %}{{ event.start_datetime }}{% endif %}
{% else %}
{{ event.get_recurrence_display }}
{% if event.label %}<span class="event-title">{{ event.label }}</span> / {% endif %}
{% endif %}
<span class="event-date">
{% if not event.recurrence_days %}
{% if view_mode == 'day_view' %}{{ event.start_datetime|time }}{% else %}{{ event.start_datetime }}{% endif %}
{% else %}
{{ event.get_recurrence_display }}
{% endif %}
</span>
{% if view_mode != 'settings_view' %}
{% if event.places or event.waiting_list_places %}-{% endif %}
{% if event.places %}
{% blocktrans count remaining_places=event.remaining_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
({% blocktrans with places=event.places count booked_places=event.booked_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% endif %}
{% if event.waiting_list_places %}
({% trans "Waiting list:" %}
{% blocktrans count remaining_places=event.remaining_waiting_list_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
-
{% blocktrans with places=event.waiting_list_places count booked_places=event.booked_waiting_list_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% endif %}
<span class="event-capacity">
{% if event.places or event.waiting_list_places %}-{% endif %}
{% if event.places %}
{% blocktrans count remaining_places=event.remaining_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
({% blocktrans with places=event.places count booked_places=event.booked_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% endif %}
{% if event.waiting_list_places %}
({% trans "Waiting list:" %}
{% blocktrans count remaining_places=event.remaining_waiting_list_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
-
{% blocktrans with places=event.waiting_list_places count booked_places=event.booked_waiting_list_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% endif %}
</span>
{% endif %}
{% if view_mode == 'settings_view' and event.publication_datetime %}
({% trans "publication date:" %} {{ event.publication_datetime }})
{% endif %}
</span>
<br />
<span class="tags">
{% if event.cancellation_status %}
<span class="tag">{{ event.cancellation_status }}</span>
{% else %}
{% if event.main_list_full %}
<span class="full tag">{% trans "Full" %}</span>
{% endif %}
{% if event.checked %}
<span class="checked tag">{% trans "Checked" %}</span>
{% endif %}
{% if event.is_day_past %}
{% if event.present_count %}<span class="meta meta-success">{% blocktrans with count=event.present_count %}Presents {{ count }}{% endblocktrans %}</span>{% endif %}
{% if event.absent_count %}<span class="meta meta-error">{% blocktrans with count=event.absent_count %}Absents {{ count }}{% endblocktrans %}</span>{% endif %}
{% if event.notchecked_count %}<span class="meta meta-disabled">{% blocktrans with count=event.notchecked_count %}Not checked {{ count }}{% endblocktrans %}</span>{% endif %}
{% endif %}
{% endif %}
{% if not event.in_bookable_period %}
({% trans "out of bookable period" %})
{% endif %}

View File

@ -28,7 +28,7 @@
{% ifchanged event.start_datetime|date:'n' event.start_datetime|date:'y' %}
{% if not forloop.first %}</ul>{% endif %}
<h4>{{ event.start_datetime|date:'YEAR_MONTH_FORMAT'|capfirst }}</h4>
<ul class="objects-list single-links">
<ul class="objects-list single-links events-list">
{% endifchanged %}
{% include 'chrono/manager_agenda_event_fragment.html' %}
{% if forloop.last %}</ul>{% endif %}

View File

@ -23,10 +23,17 @@
{% else %}
{{ event.start_datetime|date:"DATETIME_FORMAT"}}
{% endif %}
</h2>
<div class="event-title-meta">
{% if event.cancellation_status %}<span class="tag">{{ event.cancellation_status }}</span>{% endif %}
{% if event.main_list_full %}<span class="tag">{% trans "Full" %}</span>{% endif %}
{% if event.checked %}<span class="tag">{% trans "Checked" %}</span>{% endif %}
</h2>
{% if event.checked %}<span class="checked tag">{% trans "Checked" %}</span>{% endif %}
{% if event.is_day_past and not event.cancelled %}
{% if event.present_count %}<span class="meta meta-success">{% blocktrans with count=event.present_count %}Presents {{ count }}{% endblocktrans %}</span>{% endif %}
{% if event.absent_count %}<span class="meta meta-error">{% blocktrans with count=event.absent_count %}Absents {{ count }}{% endblocktrans %}</span>{% endif %}
{% if event.notchecked_count %}<span class="meta meta-disabled">{% blocktrans with count=event.notchecked_count %}Not checked {{ count }}{% endblocktrans %}</span>{% endif %}
{% endif %}
</div>
{% block appbar_actions %}
<span class="actions">
{% if user_can_manage or event.agenda.booking_form_url %}

View File

@ -18,7 +18,7 @@
<h3>{% trans "Events" %}</h3>
<div>
{% if object_list %}
<ul class="objects-list single-links">
<ul class="objects-list single-links events-list">
{% for event in object_list %}
{% include 'chrono/manager_agenda_event_fragment.html' with view_mode='day_view' %}
{% endfor %}

View File

@ -19,7 +19,7 @@
{% include 'chrono/manager_event_cancellation_report_notice.html' %}
<div>
{% if object_list %}
<ul class="objects-list single-links">
<ul class="objects-list single-links events-list">
{% for object in object_list %}
{% if object.is_exception %}
<li><a class="disabled">{% trans "Exception:"%} {{ object }}</a></li>

View File

@ -27,7 +27,7 @@
<div aria-labelledby="tab-events" id="panel-events" role="tabpanel" tabindex="0">
{% with view.get_events as events %}
{% if events %}
<ul class="objects-list single-links">
<ul class="objects-list single-links events-list">
{% for event in events %}
{% include 'chrono/manager_agenda_event_fragment.html' with view_mode='settings_view' %}
{% endfor %}

View File

@ -19,7 +19,7 @@
{% include 'chrono/manager_event_cancellation_report_notice.html' %}
<div>
{% if object_list %}
<ul class="objects-list single-links">
<ul class="objects-list single-links events-list">
{% for object in object_list %}
{% if object.is_exception %}
<li><a class="disabled">{% trans "Exception:"%} {{ object }}</a></li>

View File

@ -1315,6 +1315,7 @@ class AgendaDateView(DateMixin, ViewableAgendaMixin):
def get_queryset(self):
if self.agenda.kind == 'events':
queryset = self.agenda.event_set.filter(recurrence_days__isnull=True)
queryset = Event.annotate_booking_checks(queryset)
else:
self.agenda.prefetch_desks_and_exceptions(
min_date=getattr(self, 'first_day', self.date), max_date=self.get_max_date()
@ -2445,13 +2446,16 @@ class EventDetailView(ViewableAgendaMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
event = self.get_object()
event = self.object
context['booked'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=False
).order_by('creation_datetime')
context['waiting'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=True
).order_by('creation_datetime')
event.present_count = len([b for b in context['booked'] if b.user_was_present is True])
event.absent_count = len([b for b in context['booked'] if b.user_was_present is False])
event.notchecked_count = len([b for b in context['booked'] if b.user_was_present is None])
return context
@ -2525,6 +2529,7 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
def get_queryset(self):
queryset = super().get_queryset()
queryset = Event.annotate_booking_checks(queryset)
return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
def get_filters(self, booked_queryset, subscription_queryset):

View File

@ -1211,12 +1211,12 @@ def test_agenda_events_day_view(app, admin_user):
resp = app.get('/manage/agendas/%s/day/2020/11/11/' % agenda.pk)
assert len(ctx.captured_queries) == 5
assert len(resp.pyquery.find('.event-info')) == 2
assert 'abc' in resp.pyquery.find('.event-info')[0].text
assert 'xyz' in resp.pyquery.find('.event-info')[1].text
assert len(resp.pyquery.find('.event-title')) == 2
assert resp.pyquery.find('.event-title')[0].text.strip() == 'abc'
assert resp.pyquery.find('.event-title')[1].text.strip() == 'xyz'
resp = app.get('/manage/agendas/%s/day/2020/11/11/' % agenda.pk)
assert len(resp.pyquery.find('.event-info')) == 2
assert len(resp.pyquery.find('.event-title')) == 2
# create another event with recurrence, the same day/time
start_datetime = localtime().replace(day=4, month=11, year=2020)
@ -1230,7 +1230,7 @@ def test_agenda_events_day_view(app, admin_user):
)
event.create_all_recurrences()
resp = app.get('/manage/agendas/%s/day/2020/11/11/' % agenda.pk)
assert len(resp.pyquery.find('.event-info')) == 3
assert len(resp.pyquery.find('.event-title')) == 3
def test_agenda_events_day_view_midnight(app, admin_user):
@ -1242,7 +1242,7 @@ def test_agenda_events_day_view_midnight(app, admin_user):
resp = app.get('/manage/agendas/%s/day/' % agenda.pk)
assert resp.location.endswith('day/2020/11/11/')
resp = resp.follow()
assert len(resp.pyquery.find('.event-info')) == 1
assert len(resp.pyquery.find('.event-title')) == 1
@freezegun.freeze_time('2020-10-01')
@ -1281,9 +1281,9 @@ def test_agenda_events_week_view(app, admin_user):
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.pk, 2020, 11, 11))
assert len(ctx.captured_queries) == 7
assert len(resp.pyquery.find('.event-info')) == 2
assert 'abc' in resp.pyquery.find('.event-info')[0].text
assert 'xyz' in resp.pyquery.find('.event-info')[1].text
assert len(resp.pyquery.find('.event-title')) == 2
assert resp.pyquery.find('.event-title')[0].text.strip() == 'abc'
assert resp.pyquery.find('.event-title')[1].text.strip() == 'xyz'
TimePeriodException.objects.create(
desk=agenda.desk_set.get(),
@ -1292,7 +1292,7 @@ def test_agenda_events_week_view(app, admin_user):
)
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.pk, 2020, 11, 11))
assert len(resp.pyquery.find('.event-info')) == 1
assert len(resp.pyquery.find('.event-title')) == 1
assert 'Exception: 11/10/2020' in resp.pyquery.find('li')[4].text_content()
assert 'xyz' in resp.pyquery.find('li')[5].text_content()
@ -1308,7 +1308,7 @@ def test_agenda_events_week_view(app, admin_user):
)
event.create_all_recurrences()
resp = app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.pk, 2020, 12, 7))
assert len(resp.pyquery.find('.event-info')) == 2
assert len(resp.pyquery.find('.event-title')) == 2
time = localtime(event.start_datetime).strftime('%H%M')
resp = resp.click('Dec. 9, 2020, 1 a.m.', index=1)
@ -1318,7 +1318,7 @@ def test_agenda_events_week_view(app, admin_user):
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/week/%s/%s/%s/' % (agenda.pk, 2020, 12, 7))
assert len(resp.pyquery.find('.event-info')) == 2
assert len(resp.pyquery.find('.event-title')) == 2
assert '1:12 p.m.' in resp.text
Event.objects.get(slug='abc--2020-12-02-%s' % time).cancel()
@ -1335,7 +1335,7 @@ def test_agenda_events_week_view_midnight(app, admin_user):
resp = app.get('/manage/agendas/%s/week/' % agenda.pk)
assert resp.location.endswith('week/2020/11/01/')
resp = resp.follow()
assert len(resp.pyquery.find('.event-info')) == 1
assert len(resp.pyquery.find('.event-title')) == 1
@freezegun.freeze_time('2020-10-01')
@ -1374,12 +1374,12 @@ def test_agenda_events_month_view(app, admin_user):
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, 2020, 11, 1))
assert len(ctx.captured_queries) == 7
assert len(resp.pyquery.find('.event-info')) == 5
assert 'abc' in resp.pyquery.find('.event-info')[0].text
assert 'abc' in resp.pyquery.find('.event-info')[1].text
assert 'xyz' in resp.pyquery.find('.event-info')[2].text
assert 'abc' in resp.pyquery.find('.event-info')[3].text
assert 'abc' in resp.pyquery.find('.event-info')[4].text
assert len(resp.pyquery.find('.event-title')) == 5
assert resp.pyquery.find('.event-title')[0].text.strip() == 'abc'
assert resp.pyquery.find('.event-title')[1].text.strip() == 'abc'
assert resp.pyquery.find('.event-title')[2].text.strip() == 'xyz'
assert resp.pyquery.find('.event-title')[3].text.strip() == 'abc'
assert resp.pyquery.find('.event-title')[4].text.strip() == 'abc'
TimePeriodException.objects.create(
desk=agenda.desk_set.get(),
@ -1388,14 +1388,14 @@ def test_agenda_events_month_view(app, admin_user):
)
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, 2020, 11, 1))
assert len(resp.pyquery.find('.event-info')) == 4
assert len(resp.pyquery.find('.event-title')) == 4
assert 'abc' in resp.pyquery.find('li')[4].text_content()
assert 'Exception: 11/10/2020' in resp.pyquery.find('li')[5].text_content()
assert 'xyz' in resp.pyquery.find('li')[6].text_content()
# 12/2020 has 5 Wednesday
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, 2020, 12, 1))
assert len(resp.pyquery.find('.event-info')) == 5
assert len(resp.pyquery.find('.event-title')) == 5
# create another event with recurrence, the same day/time
start_datetime = localtime().replace(day=4, month=11, year=2020)
@ -1409,7 +1409,7 @@ def test_agenda_events_month_view(app, admin_user):
)
event.create_all_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, 2020, 12, 1))
assert len(resp.pyquery.find('.event-info')) == 10
assert len(resp.pyquery.find('.event-title')) == 10
time = localtime(event.start_datetime).strftime('%H%M')
resp = resp.click('Dec. 9, 2020, 1 a.m.', index=1)
@ -1419,7 +1419,7 @@ def test_agenda_events_month_view(app, admin_user):
agenda.update_event_recurrences()
resp = app.get('/manage/agendas/%s/month/%s/%s/%s/' % (agenda.pk, 2020, 12, 1))
assert len(resp.pyquery.find('.event-info')) == 10
assert len(resp.pyquery.find('.event-title')) == 10
assert '1:12 p.m.' in resp.text
Event.objects.get(slug='abc--2020-12-02-%s' % time).cancel()
@ -1439,7 +1439,7 @@ def test_agenda_events_month_view_midnight(app, admin_user):
resp = app.get('/manage/agendas/%s/month/' % agenda.pk)
assert resp.location.endswith('month/2020/11/01/')
resp = resp.follow()
assert len(resp.pyquery.find('.event-info')) == 1
assert len(resp.pyquery.find('.event-title')) == 1
def test_agenda_open_events_view(app, admin_user, manager_user):

View File

@ -1673,16 +1673,37 @@ def test_event_checked(app, admin_user):
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert 'Mark the event as checked' not in resp
Booking.objects.create(event=event, user_first_name='User')
for i in range(8):
user_was_present = None
if i < 3:
user_was_present = True
elif i < 7:
user_was_present = False
Booking.objects.create(
event=event,
user_was_present=user_was_present,
)
resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
assert '<span class="tag">Checked</span>' not in resp
assert 'Mark the event as checked' in resp
assert event.checked is False
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'checked tag' not in resp
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '<span class="tag">Checked</span>' not in resp
urls = [
'/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk),
'/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk),
'/manage/agendas/%s/month/%d/%d/%d/'
% (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day),
'/manage/agendas/%s/week/%d/%d/%d/'
% (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day),
'/manage/agendas/%s/day/%d/%d/%d/'
% (agenda.pk, event.start_datetime.year, event.start_datetime.month, event.start_datetime.day),
]
for url in urls:
resp = app.get(url)
assert '<span class="checked tag">Checked</span>' not in resp
assert '<span class="meta meta-success">Presents 3</span>' in resp
assert '<span class="meta meta-error">Absents 4</span>' in resp
assert '<span class="meta meta-disabled">Not checked 1</span>' in resp
token = resp.context['csrf_token']
resp = app.post(
@ -1693,12 +1714,16 @@ def test_event_checked(app, admin_user):
event.refresh_from_db()
assert event.checked is True
resp = resp.follow()
assert '<span class="tag">Checked</span>' in resp
assert 'Mark the event as checked' not in resp
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert 'checked tag' in resp
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.pk, event.pk))
assert '<span class="tag">Checked</span>' in resp
Booking.objects.filter(user_was_present__isnull=True).update(user_was_present=True)
for url in urls:
resp = app.get(url)
assert '<span class="checked tag">Checked</span>' in resp
assert '<span class="meta meta-success">Presents 4</span>' in resp
assert '<span class="meta meta-error">Absents 4</span>' in resp
assert 'meta meta-disabled' not in resp
@mock.patch('chrono.manager.forms.get_agenda_check_types')