Projet

Général

Profil

0003-manager-add-event-cancellation-44157.patch

Valentin Deniaud, 11 août 2020 17:02

Télécharger (29,9 ko)

Voir les différences:

Subject: [PATCH 3/3] manager: add event cancellation (#44157)

 .../management/commands/cancel_events.py      | 48 +++++++++
 .../migrations/0056_auto_20200811_1611.py     | 50 ++++++++++
 chrono/agendas/models.py                      | 39 ++++++++
 chrono/api/views.py                           |  4 +
 chrono/manager/forms.py                       | 33 ++++++-
 chrono/manager/static/css/style.scss          |  8 ++
 .../chrono/manager_agenda_event_fragment.html | 15 ++-
 .../manager_confirm_event_cancellation.html   | 39 ++++++++
 .../manager_event_cancellation_report.html    | 27 +++++
 ...ager_event_cancellation_report_notice.html | 11 +++
 .../manager_event_cancellation_reports.html   | 28 ++++++
 .../chrono/manager_event_detail.html          | 12 ++-
 .../manager_events_agenda_month_view.html     |  6 ++
 chrono/manager/urls.py                        | 20 ++++
 chrono/manager/views.py                       | 98 +++++++++++++++++++
 tests/test_manager.py                         | 76 ++++++++++++++
 16 files changed, 508 insertions(+), 6 deletions(-)
 create mode 100644 chrono/agendas/management/commands/cancel_events.py
 create mode 100644 chrono/agendas/migrations/0056_auto_20200811_1611.py
 create mode 100644 chrono/manager/templates/chrono/manager_confirm_event_cancellation.html
 create mode 100644 chrono/manager/templates/chrono/manager_event_cancellation_report.html
 create mode 100644 chrono/manager/templates/chrono/manager_event_cancellation_report_notice.html
 create mode 100644 chrono/manager/templates/chrono/manager_event_cancellation_reports.html
chrono/agendas/management/commands/cancel_events.py
1
# chrono - agendas system
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from requests import RequestException
18

  
19
from django.core.management.base import BaseCommand
20
from django.db import transaction
21

  
22
from chrono.agendas.models import Event, EventCancellationReport
23

  
24

  
25
class Command(BaseCommand):
26
    help = 'Cancel events and related bookings'
27

  
28
    def handle(self, **options):
29
        for event in Event.objects.filter(cancellation_scheduled=True):
30
            errors = {}
31
            bookings = []
32
            for booking in event.booking_set.filter(cancellation_datetime__isnull=True).all():
33
                try:
34
                    booking.cancel()
35
                except RequestException as e:
36
                    bookings.append(booking)
37
                    errors[booking.pk] = str(e)
38

  
39
            if not errors:
40
                event.cancelled = True
41
                event.cancellation_scheduled = False
42
                event.save()
43
            else:
44
                with transaction.atomic():
45
                    report = EventCancellationReport.objects.create(event=event, booking_errors=errors)
46
                    report.bookings.set(bookings)
47
                    event.cancellation_scheduled = False
48
                    event.save()
chrono/agendas/migrations/0056_auto_20200811_1611.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-08-11 14:11
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import jsonfield.fields
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('agendas', '0055_booking_cancel_callback_url'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='EventCancellationReport',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                ('timestamp', models.DateTimeField(auto_now_add=True)),
25
                ('seen', models.BooleanField(default=False)),
26
                ('booking_errors', jsonfield.fields.JSONField(default=dict)),
27
                ('bookings', models.ManyToManyField(to='agendas.Booking')),
28
            ],
29
            options={'ordering': ['-timestamp'],},
30
        ),
31
        migrations.AddField(
32
            model_name='event', name='cancellation_scheduled', field=models.BooleanField(default=False),
33
        ),
34
        migrations.AddField(
35
            model_name='event',
36
            name='cancelled',
37
            field=models.BooleanField(
38
                default=False, help_text="Cancel this event so that it won't be bookable anymore."
39
            ),
40
        ),
41
        migrations.AddField(
42
            model_name='eventcancellationreport',
43
            name='event',
44
            field=models.ForeignKey(
45
                on_delete=django.db.models.deletion.CASCADE,
46
                related_name='cancellation_reports',
47
                to='agendas.Event',
48
            ),
49
        ),
50
    ]
chrono/agendas/models.py
434 434
        assert self.kind == 'events'
435 435

  
436 436
        entries = self.event_set.all()
437
        entries = self.event_set.filter(cancelled=False)
437 438
        # we never want to allow booking for past events.
438 439
        entries = entries.filter(start_datetime__gte=localtime(now()))
439 440
        # exclude non published events
......
773 774
    pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
774 775
    url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
775 776
    full = models.BooleanField(default=False)
777
    cancelled = models.BooleanField(
778
        default=False, help_text=_("Cancel this event so that it won't be bookable anymore.")
779
    )
780
    cancellation_scheduled = models.BooleanField(default=False)
776 781
    meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE)
777 782
    desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
778 783
    resources = models.ManyToManyField('Resource')
......
786 791
            return self.label
787 792
        return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
788 793

  
794
    @functional.cached_property
795
    def cancellation_status(self):
796
        if self.cancelled:
797
            return _('Cancelled')
798
        if self.cancellation_scheduled:
799
            return _('Cancellation in progress')
800

  
789 801
    def save(self, *args, **kwargs):
790 802
        assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
791 803
        assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number'
......
932 944

  
933 945
        return new_event
934 946

  
947
    def cancel(self, cancel_bookings=True):
948
        bookings_to_cancel = self.booking_set.filter(cancellation_datetime__isnull=True).all()
949
        if cancel_bookings and bookings_to_cancel.exclude(cancel_callback_url='').exists():
950
            # booking cancellation needs network calls, schedule it for later
951
            self.cancellation_scheduled = True
952
            self.save()
953
        else:
954
            with transaction.atomic():
955
                for booking in bookings_to_cancel:
956
                    booking.cancel(trigger_callback=False)
957
                self.cancelled = True
958
                self.save()
959

  
935 960

  
936 961
class Booking(models.Model):
937 962
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
......
1433 1458
    def as_interval(self):
1434 1459
        '''Simplify insertion into IntervalSet'''
1435 1460
        return Interval(self.start_datetime, self.end_datetime)
1461

  
1462

  
1463
class EventCancellationReport(models.Model):
1464
    event = models.ForeignKey(Event, related_name='cancellation_reports')
1465
    timestamp = models.DateTimeField(auto_now_add=True)
1466
    seen = models.BooleanField(default=False)
1467
    bookings = models.ManyToManyField(Booking)
1468
    booking_errors = JSONField()
1469

  
1470
    def __str__(self):
1471
        return '%s - %s' % (self.timestamp.strftime('%Y-%m-%d %H:%M:%S'), self.event)
1472

  
1473
    class Meta:
1474
        ordering = ['-timestamp']
chrono/api/views.py
925 925
                    return Response(
926 926
                        {'err': 1, 'err_class': 'event not bookable', 'err_desc': _('event not bookable')}
927 927
                    )
928
                if event.cancelled:
929
                    return Response(
930
                        {'err': 1, 'err_class': 'event is cancelled', 'err_desc': _('event is cancelled')}
931
                    )
928 932

  
929 933
            if not events.count():
930 934
                return Response(
chrono/manager/forms.py
115 115
            'start_datetime': DateTimeWidget(),
116 116
            'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
117 117
        }
118
        exclude = ['full', 'meeting_type', 'desk', 'slug', 'resources']
118
        fields = [
119
            'agenda',
120
            'start_datetime',
121
            'duration',
122
            'publication_date',
123
            'places',
124
            'waiting_list_places',
125
            'label',
126
            'description',
127
            'pricing',
128
            'url',
129
        ]
119 130

  
120 131

  
121 132
class EventForm(forms.ModelForm):
......
126 137
            'start_datetime': DateTimeWidget(),
127 138
            'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
128 139
        }
129
        exclude = ['full', 'meeting_type', 'desk', 'resources']
140
        fields = [
141
            'agenda',
142
            'start_datetime',
143
            'duration',
144
            'publication_date',
145
            'places',
146
            'waiting_list_places',
147
            'label',
148
            'slug',
149
            'description',
150
            'pricing',
151
            'url',
152
        ]
130 153

  
131 154

  
132 155
class AgendaResourceForm(forms.Form):
......
473 496
    class Meta:
474 497
        model = Booking
475 498
        fields = []
499

  
500

  
501
class EventCancelForm(forms.ModelForm):
502
    class Meta:
503
        model = Event
504
        fields = []
chrono/manager/static/css/style.scss
25 25
	background: #f8f8fe;
26 26
}
27 27

  
28
li.cancelled span.event-info {
29
	text-decoration: line-through;
30
}
31

  
32
li.new-report {
33
    font-weight: bold;
34
}
35

  
28 36
li span.duration {
29 37
	font-size: 80%;
30 38
}
chrono/manager/templates/chrono/manager_agenda_event_fragment.html
1 1
{% load i18n %}
2 2
<li class="{% if event.booked_places_count > event.places %}overbooking{% endif %}
3 3
           {% if event.main_list_full %}full{% endif %}
4
             {% if event.cancellation_status %}cancelled{% endif %}
4 5
           {% if not event.in_bookable_period %}not-{% endif %}bookable"
5 6
    {% if event.places %}
6 7
      data-total="{{ event.places }}" data-booked="{{ event.booked_places_count }}"
......
8 9
      data-total="{{ event.waiting_list_places }}" data-booked="{{ event.waiting_list_count }}"
9 10
    {% endif %}
10 11
    ><a href="{% if settings_view %}{% url 'chrono-manager-event-edit' pk=agenda.pk event_pk=event.pk %}?next=settings{% else %}{% url 'chrono-manager-event-view' pk=agenda.pk event_pk=event.pk %}{% endif %}">
11
    {% if event.main_list_full %}<span class="full tag">{% trans "Full" %}</span>{% endif %}
12
    {% if event.cancellation_status %}
13
    {{ event.cancellation_status }}
14
    {% elif event.main_list_full %}
15
    <span class="full tag">{% trans "Full" %}</span>
16
    {% endif %}
17
    <span class="event-info">
12 18
    {% if settings_view %}
13 19
      {% if event.label %}{{ event.label }} {% endif %}[{% trans "identifier:" %} {{ event.slug }}]
14 20
    {% else %}
......
32 38
    {% if not event.in_bookable_period %}
33 39
    ({% trans "out of bookable period" %})
34 40
    {% endif %}
41
    </span>
35 42
        </a>
36
    {% if settings_view %}<a rel="popup" class="delete" href="{% url 'chrono-manager-event-delete' pk=agenda.pk event_pk=event.pk %}?next=settings">{% trans "remove" %}</a>{% endif %}
43
    {% if settings_view %}
44
    <a rel="popup" class="delete" href="{% url 'chrono-manager-event-delete' pk=agenda.pk event_pk=event.pk %}?next=settings">{% trans "remove" %}</a>
45
    {% elif not event.cancellation_status %}
46
    <a rel="popup" class="link-action-text cancel" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
47
    {% endif %}
37 48
    <span class="occupation-bar"></span>
38 49
</li>
chrono/manager/templates/chrono/manager_confirm_event_cancellation.html
1
{% extends "chrono/manager_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{{ view.model.get_verbose_name }}</h2>
6
{% endblock %}
7

  
8
{% block content %}
9

  
10
<form method="post">
11
  {% if cancellation_forbidden %}
12
  <div class="warningnotice">
13
    {% blocktrans trimmed %}
14
    This event has bookings with no callback url configured. Their cancellation must be
15
    handled individually from the forms attached to them. Only then, cancelling this event
16
    will be allowed.
17
    {% endblocktrans %}
18
  </div>
19
  {% else %}
20
  {% csrf_token %}
21
  <p>
22
  {% trans "Are you sure you want to cancel this event?" %}
23
  {% if bookings_count %}
24
  {% if cancel_bookings %}
25
  {% blocktrans %}The {{ bookings_count }} related bookings will also be cancelled.{% endblocktrans %}
26
  {% else %}
27
  {% blocktrans %}Related bookings will have to be manually cancelled if needed.{% endblocktrans %}
28
  {% endif %}
29
  {% endif %}
30
  </p>
31
  <input type="hidden" name="next" value="{% firstof request.POST.next request.GET.next %}">
32
  {{ form.as_p }}
33
  <div class="buttons">
34
    <button class="delete-button">{% trans "Proceed with cancellation" %}</button>
35
    <a class="cancel" href="{{ view.get_success_url }}">{% trans 'Abort' %}</a>
36
  </div>
37
  {% endif %}
38
</form>
39
{% endblock %}
chrono/manager/templates/chrono/manager_event_cancellation_report.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
3

  
4
{% block page-title-extra-label %}
5
{{ block.super }} - {% trans "Cancellation error report" %}
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="{% url 'chrono-manager-event-cancellation-report-list' pk=agenda.id %}">{% trans "Cancellation error reports" %}</a>
11
{% endblock %}
12

  
13
{% block appbar %}
14
<h2>{% trans "Cancellation error report:" %} {{ report }}</h2>
15
{% block actions %}
16
<a rel="popup" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=report.event.pk %}?force_cancellation=True">{% trans "Force cancellation" %}</a>
17
{% endblock %}
18
{% endblock %}
19

  
20
{% block content %}
21
<p>{% trans "Cancellation failed for the following bookings :" %}</p>
22
<ul>
23
{% for booking, error in errors.items %}
24
<li><a href="{{ booking.backoffice_url }}">{{ booking.events_display }}</a>: {{ error }}</li>
25
{% endfor %}
26
</ul>
27
{% endblock %}
chrono/manager/templates/chrono/manager_event_cancellation_report_notice.html
1
{% load i18n %}
2

  
3
{% for report in cancellation_reports %}
4
<div class="warningnotice">
5
<p>
6
{% blocktrans %}Errors occured during cancellation of event {{ report.event }}.{% endblocktrans %}
7
<a href="{% url 'chrono-manager-event-cancellation-report' pk=agenda.pk report_pk=report.pk %}">{% trans "Details" %}</a>
8
<a href="{% url 'chrono-manager-event-cancellation-report-seen' pk=agenda.pk report_pk=report.pk %}?next={{ request.path }}">{% trans "Mark as seen" %}</a>
9
</p>
10
</div>
11
{% endfor %}
chrono/manager/templates/chrono/manager_event_cancellation_reports.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
3

  
4
{% block page-title-extra-label %}
5
{{ block.super }} - {% trans "Cancellation error reports" %}
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="{% url 'chrono-manager-event-cancellation-report-list' pk=agenda.pk %}">{% trans "Cancellation error reports" %}</a>
11
{% endblock %}
12

  
13
{% block appbar %}
14
<h2>{% trans "Cancellation error reports" %}</h2>
15
{% endblock %}
16

  
17
{% block content %}
18
<ul>
19
{% for report in cancellation_reports %}
20
<li {% if not report.seen %}class="new-report"{% endif %}>
21
  <a href="{% url 'chrono-manager-event-cancellation-report' pk=agenda.pk report_pk=report.pk %}">{{ report }}</a>
22
  ({% blocktrans with count=report.bookings.count %}{{ count }} failures{% endblocktrans %})
23
</li>
24
{% empty %}
25
<li>{% trans "There has been no errors cancelling events." %}</li>
26
{% endfor %}
27
</ul>
28
{% endblock %}
chrono/manager/templates/chrono/manager_event_detail.html
17 17
{% endblock %}
18 18

  
19 19
{% block appbar %}
20
{% if object.label %}<h2>{{ object.label }} — {{object.start_datetime|date:"DATETIME_FORMAT"}}</h2>
21
{% else %}<h2>{{ object.start_datetime|date:"DATETIME_FORMAT"}}</h2>
20
<h2>
21
{% if object.label %}
22
{{ object.label }} — {{object.start_datetime|date:"DATETIME_FORMAT"}}
23
{% else %}
24
{{ object.start_datetime|date:"DATETIME_FORMAT"}}
22 25
{% endif %}
26
{% if object.cancellation_status %}
27
({{ object.cancellation_status }})
28
{% endif %}
29
</h2>
23 30
<span class="actions">
24 31
{% if user_can_manage %}
25 32
<a rel="popup" href="{% url 'chrono-manager-event-delete' pk=object.agenda.id event_pk=object.id %}">{% trans 'Delete' %}</a>
33
<a rel="popup" class="link-action-text cancel" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
26 34
<a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a>
27 35
{% endif %}
28 36
</span>
chrono/manager/templates/chrono/manager_events_agenda_month_view.html
2 2
{% load i18n %}
3 3

  
4 4
{% block actions %}
5
<a class="extra-actions-menu-opener"></a>
6
{{ block.super }}
5 7
<a href="{% url 'chrono-manager-agenda-open-events-view' pk=agenda.pk %}">{% trans 'Open events' %}</a>
8
<ul class="extra-actions-menu">
9
  <li><a href="{% url 'chrono-manager-event-cancellation-report-list' pk=agenda.pk %}">{% trans 'Cancellation error reports' %}</a></li>
10
</ul>
6 11
{% endblock %}
7 12

  
8 13
{% block content %}
9 14
<div class="section">
10 15
<h3>{% trans "Events" %}</h3>
16
{% include 'chrono/manager_event_cancellation_report_notice.html' %}
11 17
<div>
12 18
{% if object_list %}
13 19
  <ul class="objects-list single-links">
chrono/manager/urls.py
88 88
        views.event_delete,
89 89
        name='chrono-manager-event-delete',
90 90
    ),
91
    url(
92
        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/cancel$',
93
        views.event_cancel,
94
        name='chrono-manager-event-cancel',
95
    ),
96
    url(
97
        r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/$',
98
        views.event_cancellation_report,
99
        name='chrono-manager-event-cancellation-report',
100
    ),
101
    url(
102
        r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/seen$',
103
        views.event_cancellation_report_seen,
104
        name='chrono-manager-event-cancellation-report-seen',
105
    ),
106
    url(
107
        r'^agendas/(?P<pk>\d+)/event_cancellation_reports/$',
108
        views.event_cancellation_report_list,
109
        name='chrono-manager-event-cancellation-report-list',
110
    ),
91 111
    url(
92 112
        r'^agendas/(?P<pk>\d+)/add-resource/$',
93 113
        views.agenda_add_resource,
chrono/manager/views.py
62 62
    VirtualMember,
63 63
    Resource,
64 64
    Category,
65
    EventCancellationReport,
65 66
)
66 67

  
67 68
from .forms import (
......
88 89
    CategoryAddForm,
89 90
    CategoryEditForm,
90 91
    BookingCancelForm,
92
    EventCancelForm,
91 93
)
92 94
from .utils import import_site
93 95

  
......
952 954
        context = super(AgendaMonthView, self).get_context_data(**kwargs)
953 955
        if self.agenda.kind == 'meetings':
954 956
            context['single_desk'] = bool(self.agenda.prefetched_desks)
957
        elif self.agenda.kind == 'events':
958
            context['cancellation_reports'] = EventCancellationReport.objects.filter(
959
                event__agenda=self.agenda, seen=False,
960
            ).all()
955 961
        return context
956 962

  
957 963
    def get_previous_month_url(self):
......
1863 1869
booking_cancel = BookingCancelView.as_view()
1864 1870

  
1865 1871

  
1872
class EventCancelView(ViewableAgendaMixin, UpdateView):
1873
    template_name = 'chrono/manager_confirm_event_cancellation.html'
1874
    model = Event
1875
    pk_url_kwarg = 'event_pk'
1876
    form_class = EventCancelForm
1877

  
1878
    def dispatch(self, request, *args, **kwargs):
1879
        self.event = self.get_object()
1880
        if self.event.cancellation_status:
1881
            raise PermissionDenied()
1882
        self.cancel_bookings = False if self.request.GET.get('force_cancellation') else True
1883
        return super().dispatch(request, *args, **kwargs)
1884

  
1885
    def form_valid(self, form):
1886
        self.event.cancel(self.cancel_bookings)
1887
        return HttpResponseRedirect(self.get_success_url())
1888

  
1889
    def get_context_data(self, **kwargs):
1890
        context = super().get_context_data(**kwargs)
1891
        context['cancel_bookings'] = self.cancel_bookings
1892
        context['bookings_count'] = self.event.booking_set.filter(cancellation_datetime__isnull=True).count()
1893
        context['cancellation_forbidden'] = (
1894
            self.event.booking_set.filter(cancel_callback_url='').exclude(backoffice_url='').exists()
1895
        )
1896
        return context
1897

  
1898
    def get_success_url(self):
1899
        self.event.refresh_from_db()
1900
        if self.event.cancellation_scheduled:
1901
            messages.info(self.request, _('Event "%s" will be cancelled in a few minutes.') % self.event)
1902
        next_url = self.request.POST.get('next')
1903
        if next_url:
1904
            return next_url
1905
        day = self.event.start_datetime
1906
        return reverse(
1907
            'chrono-manager-agenda-month-view',
1908
            kwargs={'pk': self.event.agenda.pk, 'year': day.year, 'month': day.month},
1909
        )
1910

  
1911

  
1912
event_cancel = EventCancelView.as_view()
1913

  
1914

  
1915
class EventCancellationReportView(ViewableAgendaMixin, DetailView):
1916
    model = EventCancellationReport
1917
    template_name = 'chrono/manager_event_cancellation_report.html'
1918
    context_object_name = 'report'
1919
    pk_url_kwarg = 'report_pk'
1920

  
1921
    def get(self, *args, **kwargs):
1922
        self.report = self.get_object()
1923
        self.report.seen = True
1924
        self.report.save()
1925
        return super().get(*args, **kwargs)
1926

  
1927
    def get_context_data(self, **kwargs):
1928
        context = super().get_context_data(**kwargs)
1929
        bookings = self.report.bookings.all()
1930
        errors = self.report.booking_errors
1931
        context['errors'] = {
1932
            booking: errors[str(booking.pk)] for booking in bookings if str(booking.pk) in errors
1933
        }
1934
        return context
1935

  
1936

  
1937
event_cancellation_report = EventCancellationReportView.as_view()
1938

  
1939

  
1940
class EventCancellationReportSeenView(ViewableAgendaMixin, DetailView):
1941
    model = EventCancellationReport
1942
    pk_url_kwarg = 'report_pk'
1943

  
1944
    def get(self, *args, **kwargs):
1945
        report = self.get_object()
1946
        report.seen = True
1947
        report.save()
1948
        next_url = self.request.GET.get('next')
1949
        return HttpResponseRedirect(next_url)
1950

  
1951

  
1952
event_cancellation_report_seen = EventCancellationReportSeenView.as_view()
1953

  
1954

  
1955
class EventCancellationReportListView(ViewableAgendaMixin, ListView):
1956
    model = EventCancellationReport
1957
    context_object_name = 'cancellation_reports'
1958
    template_name = 'chrono/manager_event_cancellation_reports.html'
1959

  
1960

  
1961
event_cancellation_report_list = EventCancellationReportListView.as_view()
1962

  
1963

  
1866 1964
def menu_json(request):
1867 1965
    label = _('Agendas')
1868 1966
    json_str = json.dumps(
tests/test_manager.py
3706 3706

  
3707 3707
    resp = resp.follow()
3708 3708
    assert 'Bookings (0/10)' in resp.text
3709

  
3710

  
3711
def test_event_cancellation(app, admin_user):
3712
    agenda = Agenda.objects.create(label='Events', kind='events')
3713
    event = Event.objects.create(
3714
        label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
3715
    )
3716
    day = event.start_datetime
3717

  
3718
    login(app)
3719
    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
3720
    assert '0/10 bookings' in resp.text
3721

  
3722
    resp = resp.click('Cancel')
3723
    assert not 'related bookings' in resp.text
3724

  
3725
    booking = Booking.objects.create(event=event)
3726
    booking2 = Booking.objects.create(event=event)
3727

  
3728
    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
3729
    assert '2/10 bookings' in resp.text
3730

  
3731
    resp = resp.click('Cancel')
3732
    assert '2 related bookings will also be cancelled.' in resp.text
3733

  
3734
    resp = resp.form.submit().follow()
3735
    assert 'Cancelled' in resp.text
3736
    assert '0/10 bookings' in resp.text
3737
    assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2
3738

  
3739

  
3740
def test_event_cancellation_callback_error(app, admin_user):
3741
    agenda = Agenda.objects.create(label='Events', kind='events')
3742
    event = Event.objects.create(
3743
        label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
3744
    )
3745
    booking = Booking.objects.create(event=event)
3746
    booking2 = Booking.objects.create(event=event, cancel_callback_url='http://example.org/jump/trigger/')
3747
    day = event.start_datetime
3748

  
3749
    def mocked_requests_connection_error(*args, **kwargs):
3750
        raise requests.exceptions.ConnectionError('unreachable')
3751

  
3752
    login(app)
3753
    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
3754
    resp = resp.click('Cancel')
3755
    assert resp.form['disable_trigger'].attrs['type'] == 'hidden'
3756

  
3757
    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
3758
        mock_response = mock.Mock(status_code=200)
3759
        mock_send.return_value = mock_response
3760
        mock_send.side_effect = mocked_requests_connection_error
3761
        resp = resp.form.submit()
3762

  
3763
    assert 'error' in resp.text
3764
    assert not Booking.objects.filter(cancellation_datetime__isnull=False).exists()
3765

  
3766
    resp.form['disable_trigger'] = True
3767
    resp = resp.form.submit()
3768
    assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 2
3769

  
3770

  
3771
def test_event_cancellation_forbidden(app, admin_user):
3772
    agenda = Agenda.objects.create(label='Events', kind='events')
3773
    event = Event.objects.create(
3774
        label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
3775
    )
3776
    booking = Booking.objects.create(event=event)
3777
    booking2 = Booking.objects.create(event=event, backoffice_url='http://example.org/backoffice/xx/')
3778
    day = event.start_datetime
3779

  
3780
    login(app)
3781
    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
3782
    resp = resp.click('Cancel')
3783
    assert 'event has bookings with no callback url configured' in resp.text
3784
    assert 'Proceed with cancellation' not in resp.text
3709
-