diff --git a/chrono/agendas/migrations/0149_booking_extra_user_block.py b/chrono/agendas/migrations/0149_booking_extra_user_block.py new file mode 100644 index 00000000..a9a083c9 --- /dev/null +++ b/chrono/agendas/migrations/0149_booking_extra_user_block.py @@ -0,0 +1,22 @@ +from django.db import migrations, models + +import chrono.agendas.models + + +class Migration(migrations.Migration): + dependencies = [ + ('agendas', '0148_agenda_minimal_booking_time'), + ] + + operations = [ + migrations.AddField( + model_name='agenda', + name='booking_extra_user_block_template', + field=models.TextField( + blank=True, + help_text='Displayed on check page', + validators=[chrono.agendas.models.django_template_validator], + verbose_name='Extra user block template', + ), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 12f397b5..fe7fdcfc 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -51,7 +51,14 @@ from django.db.models import ( Value, ) from django.db.models.functions import Cast, Coalesce, Concat, ExtractWeek, ExtractWeekDay, JSONObject -from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines +from django.template import ( + Context, + RequestContext, + Template, + TemplateSyntaxError, + VariableDoesNotExist, + engines, +) from django.urls import reverse from django.utils import functional from django.utils.dates import WEEKDAYS @@ -240,6 +247,12 @@ class Agenda(models.Model): blank=True, validators=[django_template_validator], ) + booking_extra_user_block_template = models.TextField( + _('Extra user block template'), + blank=True, + validators=[django_template_validator], + help_text=_('Displayed on check page'), + ) event_display_template = models.CharField( _('Event display template'), max_length=256, @@ -2240,6 +2253,18 @@ class Booking(models.Model): except (VariableDoesNotExist, TemplateSyntaxError): return + def get_extra_user_block(self, request): + context = RequestContext(request) + context.update( + { + 'booking': self, + } + ) + try: + return Template(self.event.agenda.booking_extra_user_block_template).render(context) + except (VariableDoesNotExist, TemplateSyntaxError): + return + @classmethod def anonymize_bookings(cls, bookings_queryset): bookings_queryset.update( @@ -3303,6 +3328,18 @@ class Subscription(models.Model): except (VariableDoesNotExist, TemplateSyntaxError): return + def get_extra_user_block(self, request): + context = RequestContext(request) + context.update( + { + 'booking': self, + } + ) + try: + return Template(self.agenda.booking_extra_user_block_template).render(context) + except (VariableDoesNotExist, TemplateSyntaxError): + return + class Person(models.Model): user_external_id = models.CharField(max_length=250, unique=True) diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index feae4064..04a035f5 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -1417,7 +1417,9 @@ class AgendaBookingCheckSettingsForm(forms.ModelForm): 'mark_event_checked_auto', 'disable_check_update', 'enable_check_for_future_events', + 'booking_extra_user_block_template', ] + widgets = {'booking_extra_user_block_template': forms.Textarea(attrs={'rows': 3})} class AgendaNotificationsForm(forms.ModelForm): diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss index ef3ab357..a7480327 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -574,3 +574,24 @@ div.agenda-settings .pk-tabs--container { } } } + +span.togglable { + padding: 1em 2ex; + cursor: pointer; +} + +.untoggled span.togglable:after { + font-family: FontAwesome; + content: "\f107"; /* angle-down */ + cursor: pointer; +} + +.toggled span.togglable:after { + font-family: FontAwesome; + content: "\f106"; /* angle-up */ + cursor: pointer; +} + +.extra-user-block { + padding-left: 2em; +} diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html index 83967ce5..5ddaff92 100644 --- a/chrono/manager/templates/chrono/manager_event_check.html +++ b/chrono/manager/templates/chrono/manager_event_check.html @@ -73,7 +73,7 @@ {% endif %} {% endif %} {% for result in results %} - + {% include "chrono/manager_event_check_booking_fragment.html" with booking=result %} {% endfor %} @@ -91,8 +91,11 @@ {% for booking in waiting %} - - + + {% endfor %} @@ -101,4 +104,31 @@ {% endif %} + + {% endblock %} diff --git a/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html b/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html index b3a114c3..1460e30a 100644 --- a/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html +++ b/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html @@ -1,6 +1,9 @@ {% load i18n %} - +
{{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %}
+ {% if agenda.booking_extra_user_block_template %}{% endif %} + {{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %} + {% if booking.cancellation_datetime %}({% trans "Cancelled" %}){% endif %}
{{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %} + {% if agenda.booking_extra_user_block_template %}{% endif %} + {{ booking.get_user_block }}{% if booking.places_count > 1 %} ({{ booking.places_count }} {% trans "places" %}){% endif %} + {% if booking.kind == "subscription" %} ({% trans "Not booked" %}) diff --git a/chrono/manager/templates/chrono/manager_events_agenda_settings.html b/chrono/manager/templates/chrono/manager_events_agenda_settings.html index 11fa6cfd..f31af097 100644 --- a/chrono/manager/templates/chrono/manager_events_agenda_settings.html +++ b/chrono/manager/templates/chrono/manager_events_agenda_settings.html @@ -114,6 +114,10 @@
  • {% trans "Automatically mark event as checked when all bookings have been checked:" %} {{ agenda.mark_event_checked_auto|yesno }}
  • {% trans "Prevent the check of bookings when event was marked as checked:" %} {{ agenda.disable_check_update|yesno }}
  • {% trans "Enable the check of bookings when event has not passed:" %} {{ agenda.enable_check_for_future_events|yesno }}
  • +
  • + {% trans "Extra user block template:" %} +
    {{ agenda.booking_extra_user_block_template }}
    +
  • {% trans 'Configure' %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index 4610e071..2d24ce6f 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -424,6 +424,16 @@ urlpatterns = [ views.subscription_absence, name='chrono-manager-subscription-absence', ), + path( + 'agendas//bookings//extra-user-block', + views.booking_extra_user_block, + name='chrono-manager-booking-extra-user-block', + ), + path( + 'agendas//subscriptions//extra-user-block', + views.subscription_extra_user_block, + name='chrono-manager-subscription-extra-user-block', + ), re_path( r'^agendas/events.csv$', views.agenda_import_events_sample_csv, diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 944d486a..9651db64 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -1141,9 +1141,6 @@ class AgendaBookingCheckSettingsView(AgendaEditView): def set_agenda(self, **kwargs): self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events') - def get_initial(self): - return {'booking_user_block_template': self.agenda.get_booking_user_block_template()} - agenda_booking_check_settings = AgendaBookingCheckSettingsView.as_view() @@ -3642,6 +3639,34 @@ class SubscriptionAbsenceView(ViewableAgendaMixin, SubscriptionCheckMixin, Absen subscription_absence = SubscriptionAbsenceView.as_view() +class BookingExtraUserBlock(ViewableAgendaMixin, View): + def get(self, request, *args, **kwargs): + booking = get_object_or_404( + Booking, + pk=kwargs['booking_pk'], + event__agenda=self.agenda, + event__cancelled=False, + primary_booking__isnull=True, + ) + return HttpResponse(booking.get_extra_user_block(request)) + + +booking_extra_user_block = BookingExtraUserBlock.as_view() + + +class SubscriptionExtraUserBlock(ViewableAgendaMixin, View): + def get(self, request, *args, **kwargs): + subscription = get_object_or_404( + Subscription, + agenda=self.agenda, + pk=kwargs['subscription_pk'], + ) + return HttpResponse(subscription.get_extra_user_block(request)) + + +subscription_extra_user_block = SubscriptionExtraUserBlock.as_view() + + class EventCancelView(ViewableAgendaMixin, UpdateView): template_name = 'chrono/manager_confirm_event_cancellation.html' model = Event diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py index a5a5a18f..b1a8f328 100644 --- a/tests/manager/test_all.py +++ b/tests/manager/test_all.py @@ -697,6 +697,24 @@ def test_options_agenda_booking_check_options(app, admin_user): agenda.refresh_from_db() assert agenda.enable_check_for_future_events is True + # check extra user block + assert agenda.booking_extra_user_block_template == '' + resp = app.get(url) + resp.form['booking_extra_user_block_template'] = '{{ booking.user_name }} Foo Bar' + resp = resp.form.submit() + agenda.refresh_from_db() + assert agenda.booking_extra_user_block_template == '{{ booking.user_name }} Foo Bar' + + invalid_templates = [ + '{{ syntax error }}', + '{{ booking.user_name|invalidfilter }}', + ] + for template in invalid_templates: + resp = app.get(url) + resp.form['booking_extra_user_block_template'] = template + resp = resp.form.submit() + assert 'syntax error' in resp.text + # check kind agenda.kind = 'meetings' agenda.save() diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py index cb3f0eec..30068802 100644 --- a/tests/manager/test_event.py +++ b/tests/manager/test_event.py @@ -2792,6 +2792,77 @@ def test_event_check_subscription(check_types, app, admin_user): ) +@mock.patch('chrono.manager.forms.get_agenda_check_types') +def test_event_check_extra_user_block(check_types, app, admin_user): + check_types.return_value = [] + agenda = Agenda.objects.create(label='Events', kind='events') + event = Event.objects.create( + label='xyz', + start_datetime=now() - datetime.timedelta(days=1), + places=10, + waiting_list_places=5, + agenda=agenda, + ) + subscription = Subscription.objects.create( + agenda=agenda, + user_external_id='user:1', + user_first_name='Subscription', + user_last_name='41', + date_start=now() - datetime.timedelta(days=1), + date_end=now(), + ) + booking = Booking.objects.create( + event=event, user_first_name='User', user_last_name='42', user_external_id='user:2' + ) + cancelled_booking = Booking.objects.create( + event=event, user_first_name='User', user_last_name='43', user_external_id='user:3' + ) + cancelled_booking.cancel() + waiting_booking = Booking.objects.create( + event=event, + user_first_name='User', + user_last_name='44', + user_external_id='user:4', + in_waiting_list=True, + ) + + login(app) + resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) + assert '/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk) not in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) not in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk) not in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk) not in resp + + agenda.booking_extra_user_block_template = '{{ booking.user_name }} Foo Bar' + agenda.save() + resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) + assert '/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk) in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk) in resp + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk) in resp + + Subscription.objects.create( + agenda=agenda, + user_external_id='user:2', + user_first_name='Subscription', + user_last_name='42', + date_start=now() - datetime.timedelta(days=1), + date_end=now(), + ) + resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk)) + # booking url, not subscription url + assert '/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk) in resp + + resp = app.get('/manage/agendas/%s/subscriptions/%s/extra-user-block' % (agenda.pk, subscription.pk)) + assert resp.text == 'Subscription 41 Foo Bar' + resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, booking.pk)) + assert resp.text == 'User 42 Foo Bar' + resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, cancelled_booking.pk)) + assert resp.text == 'User 43 Foo Bar' + resp = app.get('/manage/agendas/%s/bookings/%s/extra-user-block' % (agenda.pk, waiting_booking.pk)) + assert resp.text == 'User 44 Foo Bar' + + @mock.patch('chrono.manager.forms.get_agenda_check_types') def test_event_check_subscription_ajax(check_types, app, admin_user): check_types.return_value = [ @@ -3049,13 +3120,13 @@ def test_event_check_primary_booking(app, admin_user): assert 'Bookings (4/10)' in resp.text user_bookings = resp.pyquery.find('td.booking-username.main-list') assert len(user_bookings) == 2 - assert user_bookings[0].text == 'User 42 (3 places)' - assert user_bookings[1].text == 'John Doe' + assert user_bookings[0].text.replace('\n', '').strip() == 'User 42 (3 places)' + assert user_bookings[1].text.replace('\n', '').strip() == 'John Doe' assert 'Waiting List (2/5)' in resp.text user_bookings = resp.pyquery.find('td.booking-username.waiting') assert len(user_bookings) == 1 - assert user_bookings[0].text == 'Jane Doe (2 places)' + assert user_bookings[0].text.replace('\n', '').strip() == 'Jane Doe (2 places)' def test_duplicate_event(app, admin_user):