0001-manager-add-event-cancellation-44157.patch
chrono/agendas/migrations/0052_event_cancelled.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2020-07-09 09:54 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 | ||
7 | ||
8 |
class Migration(migrations.Migration): |
|
9 | ||
10 |
dependencies = [ |
|
11 |
('agendas', '0051_booking_cancel_callback_url'), |
|
12 |
] |
|
13 | ||
14 |
operations = [ |
|
15 |
migrations.AddField( |
|
16 |
model_name='event', |
|
17 |
name='cancelled', |
|
18 |
field=models.BooleanField( |
|
19 |
default=False, help_text="Cancel this event so that it won't be bookable anymore." |
|
20 |
), |
|
21 |
), |
|
22 |
] |
chrono/agendas/models.py | ||
---|---|---|
413 | 413 |
assert self.kind == 'events' |
414 | 414 | |
415 | 415 |
entries = self.event_set.all() |
416 |
entries = self.event_set.filter(cancelled=False) |
|
416 | 417 |
# we never want to allow booking for past events. |
417 | 418 |
entries = entries.filter(start_datetime__gte=localtime(now())) |
418 | 419 |
# exclude non published events |
... | ... | |
752 | 753 |
pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True) |
753 | 754 |
url = models.CharField(_('URL'), max_length=200, null=True, blank=True) |
754 | 755 |
full = models.BooleanField(default=False) |
756 |
cancelled = models.BooleanField( |
|
757 |
default=False, help_text=_("Cancel this event so that it won't be bookable anymore.") |
|
758 |
) |
|
755 | 759 |
meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE) |
756 | 760 |
desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE) |
757 | 761 |
resources = models.ManyToManyField('Resource') |
... | ... | |
899 | 903 | |
900 | 904 |
return new_event |
901 | 905 | |
906 |
def cancel(self): |
|
907 |
with transaction.atomic(): |
|
908 |
for booking in self.booking_set.filter(cancellation_datetime__isnull=True).all(): |
|
909 |
booking.cancel() |
|
910 |
self.cancelled = True |
|
911 |
self.save() |
|
912 | ||
902 | 913 | |
903 | 914 |
class Booking(models.Model): |
904 | 915 |
event = models.ForeignKey(Event, on_delete=models.CASCADE) |
chrono/api/views.py | ||
---|---|---|
927 | 927 |
return Response( |
928 | 928 |
{'err': 1, 'err_class': 'event not bookable', 'err_desc': _('event not bookable')} |
929 | 929 |
) |
930 |
if event.cancelled: |
|
931 |
return Response( |
|
932 |
{'err': 1, 'err_class': 'event is cancelled', 'err_desc': _('event is cancelled')} |
|
933 |
) |
|
930 | 934 | |
931 | 935 |
if not events.count(): |
932 | 936 |
return Response( |
chrono/manager/forms.py | ||
---|---|---|
90 | 90 |
'start_datetime': DateTimeWidget(), |
91 | 91 |
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
92 | 92 |
} |
93 |
exclude = ['full', 'meeting_type', 'desk', 'slug', 'resources'] |
|
93 |
exclude = ['full', 'meeting_type', 'desk', 'slug', 'resources', 'cancelled']
|
|
94 | 94 | |
95 | 95 | |
96 | 96 |
class EventForm(forms.ModelForm): |
... | ... | |
101 | 101 |
'start_datetime': DateTimeWidget(), |
102 | 102 |
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
103 | 103 |
} |
104 |
exclude = ['full', 'meeting_type', 'desk', 'resources'] |
|
104 |
exclude = ['full', 'meeting_type', 'desk', 'resources', 'cancelled']
|
|
105 | 105 | |
106 | 106 | |
107 | 107 |
class AgendaResourceForm(forms.Form): |
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 | ||
28 | 32 |
li span.duration { |
29 | 33 |
font-size: 80%; |
30 | 34 |
} |
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.cancelled %}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.cancelled %} |
|
13 |
<span class="cancelled tag">{% trans "Cancelled" %}</span> |
|
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.cancelled %} |
|
46 |
<a rel="popup" class="link-action-text cancel" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}">{% 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 |
{% csrf_token %} |
|
12 |
<p> |
|
13 |
{% trans "Are you sure you want to cancel this event?" %} |
|
14 |
{% if bookings_count %} |
|
15 |
{% blocktrans %}The {{ bookings_count }} related bookings will also be cancelled.{% endblocktrans %} |
|
16 |
{% endif %} |
|
17 |
</p> |
|
18 |
<input type="hidden" name="next" value="{% firstof request.POST.next request.GET.next %}"> |
|
19 |
<div class="buttons"> |
|
20 |
<button class="delete-button">{% trans "Proceed with cancellation" %}</button> |
|
21 |
<a class="cancel" href="{{ view.get_success_url }}">{% trans 'Abort' %}</a> |
|
22 |
</div> |
|
23 |
</form> |
|
24 |
{% endblock %} |
chrono/manager/urls.py | ||
---|---|---|
74 | 74 |
views.event_delete, |
75 | 75 |
name='chrono-manager-event-delete', |
76 | 76 |
), |
77 |
url( |
|
78 |
r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/cancel$', |
|
79 |
views.event_cancel, |
|
80 |
name='chrono-manager-event-cancel', |
|
81 |
), |
|
77 | 82 |
url( |
78 | 83 |
r'^agendas/(?P<pk>\d+)/add-resource/$', |
79 | 84 |
views.agenda_add_resource, |
chrono/manager/views.py | ||
---|---|---|
1737 | 1737 |
booking_cancel = BookingCancelView.as_view() |
1738 | 1738 | |
1739 | 1739 | |
1740 |
class EventCancelView(ManagedAgendaMixin, DeleteView): |
|
1741 |
template_name = 'chrono/manager_confirm_event_cancellation.html' |
|
1742 |
model = Event |
|
1743 |
pk_url_kwarg = 'event_pk' |
|
1744 | ||
1745 |
def dispatch(self, request, *args, **kwargs): |
|
1746 |
self.event = self.get_object() |
|
1747 |
return super().dispatch(request, *args, **kwargs) |
|
1748 | ||
1749 |
def delete(self, request, *args, **kwargs): |
|
1750 |
self.event.cancel() |
|
1751 |
return HttpResponseRedirect(self.get_success_url()) |
|
1752 | ||
1753 |
def get_success_url(self): |
|
1754 |
day = self.event.start_datetime |
|
1755 |
return reverse( |
|
1756 |
'chrono-manager-agenda-month-view', |
|
1757 |
kwargs={'pk': self.event.agenda.pk, 'year': day.year, 'month': day.month}, |
|
1758 |
) |
|
1759 | ||
1760 |
def get_context_data(self, **kwargs): |
|
1761 |
context = super().get_context_data(**kwargs) |
|
1762 |
context['bookings_count'] = self.event.booking_set.filter(cancellation_datetime__isnull=True).count() |
|
1763 |
return context |
|
1764 | ||
1765 | ||
1766 |
event_cancel = EventCancelView.as_view() |
|
1767 | ||
1768 | ||
1740 | 1769 |
def menu_json(request): |
1741 | 1770 |
label = _('Agendas') |
1742 | 1771 |
json_str = json.dumps( |
tests/test_manager.py | ||
---|---|---|
3455 | 3455 | |
3456 | 3456 |
resp = resp.follow() |
3457 | 3457 |
assert 'Bookings (0/10)' in resp.text |
3458 | ||
3459 | ||
3460 |
def test_event_cancellation(app, admin_user): |
|
3461 |
agenda = Agenda.objects.create(label='Events', kind='events') |
|
3462 |
event = Event(label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda) |
|
3463 |
event.save() |
|
3464 |
day = event.start_datetime |
|
3465 | ||
3466 |
login(app) |
|
3467 |
resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month)) |
|
3468 |
assert '0 booked place' in resp.text |
|
3469 | ||
3470 |
resp = resp.click('Cancel') |
|
3471 |
assert not 'related bookings' in resp.text |
|
3472 | ||
3473 |
booking = Booking.objects.create(event=event) |
|
3474 |
booking2 = Booking.objects.create(event=event) |
|
3475 | ||
3476 |
resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month)) |
|
3477 |
assert '2 booked places' in resp.text |
|
3478 | ||
3479 |
resp = resp.click('Cancel') |
|
3480 |
assert '2 related bookings will also be cancelled.' |
|
3481 | ||
3482 |
resp = resp.form.submit().follow() |
|
3483 |
assert 'Cancelled' in resp.text |
|
3484 |
assert '0 booked places' in resp.text |
|
3485 |
assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2 |
|
3458 |
- |