0003-manager-add-event-cancellation-44157.patch
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 |
- |