From 316a047f14f322286fbd9018af263511c4d8d754 Mon Sep 17 00:00:00 2001
From: Michael Hall
Date: Sat, 23 Jun 2018 12:04:45 -0400
Subject: [PATCH] Allow cancelling an event with a reason, notify attendees of
the change. Fixed #91
---
events/forms.py | 4 +
events/migrations/0035_add_event_status.py | 19 +++++
events/models/events.py | 15 +++-
.../emails/events/event_canceled.html | 14 ++++
.../emails/events/event_canceled.txt | 13 +++
.../get_together/events/cancel_event.html | 15 ++++
.../get_together/events/edit_event.html | 5 ++
.../get_together/events/show_event.html | 10 ++-
.../get_together/events/show_series.html | 2 +-
.../get_together/teams/show_team.html | 4 +-
get_together/urls.py | 2 +
get_together/views/events.py | 81 ++++++++++++++++++-
12 files changed, 178 insertions(+), 6 deletions(-)
create mode 100644 events/migrations/0035_add_event_status.py
create mode 100644 get_together/templates/get_together/emails/events/event_canceled.html
create mode 100644 get_together/templates/get_together/emails/events/event_canceled.txt
create mode 100644 get_together/templates/get_together/events/cancel_event.html
diff --git a/events/forms.py b/events/forms.py
index 70536c5..7ccf600 100644
--- a/events/forms.py
+++ b/events/forms.py
@@ -272,6 +272,10 @@ class NewTeamEventForm(forms.ModelForm):
class DeleteEventForm(forms.Form):
confirm = forms.BooleanField(label="Yes, delete event", required=True)
+class CancelEventForm(forms.Form):
+ confirm = forms.BooleanField(label="Yes, cancel this event", required=True)
+ reason = forms.CharField(label=_("Reason for cancellation"), widget=forms.widgets.Textarea)
+
class EventInviteMemberForm(forms.Form):
member = forms.ChoiceField(label=_(""))
diff --git a/events/migrations/0035_add_event_status.py b/events/migrations/0035_add_event_status.py
new file mode 100644
index 0000000..c4d7cec
--- /dev/null
+++ b/events/migrations/0035_add_event_status.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.0 on 2018-06-23 15:24
+
+from django.db import migrations, models
+import imagekit.models.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('events', '0034_add_imagekit_team_cover_img'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='event',
+ name='status',
+ field=models.SmallIntegerField(choices=[(-1, 'Canceled'), (0, 'Planning'), (1, 'Confirmed')], db_index=True, default=1),
+ ),
+ ]
diff --git a/events/models/events.py b/events/models/events.py
index 5ee8032..49efbc6 100644
--- a/events/models/events.py
+++ b/events/models/events.py
@@ -57,10 +57,20 @@ class PlaceSerializer(serializers.ModelSerializer):
class Event(models.Model):
+ CANCELED = -1
+ PLANNING = 0
+ CONFIRMED = 1
+
+ STATUSES = [
+ (CANCELED, _("Canceled")),
+ (PLANNING, _("Planning")),
+ (CONFIRMED, _("Confirmed")),
+ ]
name = models.CharField(max_length=150, verbose_name=_('Event Name'))
team = models.ForeignKey(Team, on_delete=models.CASCADE)
parent = models.ForeignKey('CommonEvent', related_name='participating_events', null=True, blank=True, on_delete=models.SET_NULL)
series = models.ForeignKey('EventSeries',related_name='instances', null=True, blank=True, on_delete=models.SET_NULL)
+ status = models.SmallIntegerField(choices=STATUSES, default=CONFIRMED, db_index=True)
start_time = models.DateTimeField(help_text=_('Date and time that the event starts'), verbose_name=_('Start Time'), db_index=True)
end_time = models.DateTimeField(help_text=_('Date and time that the event ends'), verbose_name=_('End Time'), db_index=True)
@@ -132,7 +142,10 @@ class Event(models.Model):
def save(self, *args, **kwargs):
super().save(*args, **kwargs) # Call the "real" save() method.
- update_event_searchable(self)
+ if self.status > self.CANCELED:
+ update_event_searchable(self)
+ else:
+ delete_event_searchable(self)
def update_event_searchable(event):
site = Site.objects.get(id=1)
diff --git a/get_together/templates/get_together/emails/events/event_canceled.html b/get_together/templates/get_together/emails/events/event_canceled.html
new file mode 100644
index 0000000..2ccf5da
--- /dev/null
+++ b/get_together/templates/get_together/emails/events/event_canceled.html
@@ -0,0 +1,14 @@
+{% extends "get_together/emails/base.html" %}
+
+{% block content %}
+Event canceled: {{event.name|striptags}}
+
+Date: {{event.local_start_time}}
+
+Canceled by: {{by}}
+
+Reason: {{reason}}
+
+View this event.
+
+{% endblock %}
diff --git a/get_together/templates/get_together/emails/events/event_canceled.txt b/get_together/templates/get_together/emails/events/event_canceled.txt
new file mode 100644
index 0000000..64d4a0a
--- /dev/null
+++ b/get_together/templates/get_together/emails/events/event_canceled.txt
@@ -0,0 +1,13 @@
+{% extends 'get_together/emails/base.txt' %}
+{% block content %}
+== Event Canceled: {{event.name|striptags}} ==
+
+Date: {{event.local_start_time}}
+
+Canceled by: {{by}}
+
+Reason: {{reason}}
+
+Click here to view this event: {{event.get_full_url}}
+
+{% endblock %}
diff --git a/get_together/templates/get_together/events/cancel_event.html b/get_together/templates/get_together/events/cancel_event.html
new file mode 100644
index 0000000..be9a0e6
--- /dev/null
+++ b/get_together/templates/get_together/events/cancel_event.html
@@ -0,0 +1,15 @@
+{% extends "get_together/base.html" %}
+
+{% block content %}
+Confirm deletion
+Are you sure you want to cancel {{event.name}}?
+
+{% endblock %}
+
diff --git a/get_together/templates/get_together/events/edit_event.html b/get_together/templates/get_together/events/edit_event.html
index f6fefd5..8f53cd5 100644
--- a/get_together/templates/get_together/events/edit_event.html
+++ b/get_together/templates/get_together/events/edit_event.html
@@ -15,6 +15,11 @@
+{% if event.status == event.CANCELED %}
+Restore
+{% else %}
+Cancel
+{% endif %}
Delete
{% endblock %}
diff --git a/get_together/templates/get_together/events/show_event.html b/get_together/templates/get_together/events/show_event.html
index 653c63f..487a328 100644
--- a/get_together/templates/get_together/events/show_event.html
+++ b/get_together/templates/get_together/events/show_event.html
@@ -102,12 +102,13 @@
{% if team.banner_img %}
-
{{ event.name }}
+
{{ event.name }}{% if event.status == event.CANCELED %} (Canceled){% endif %}
{% else %}
{{ event.name }}
{% endif %}
Hosted by {{ team.name }}
+ {% if event.status != event.CANCELED %}
{% if settings.SOCIAL_AUTH_TWITTER_KEY %}
{% endif %}
@@ -134,6 +135,7 @@
{% endif %}
{% endif %}
+ {% endif %}
{% if can_edit_event %}
{% endif %}
+ {% if event.status == event.CANCELED %}
+
+
This event has been canceled.
+
+ {% endif %}
+
{{ event.summary|markdown }}
diff --git a/get_together/templates/get_together/events/show_series.html b/get_together/templates/get_together/events/show_series.html
index ad97b96..d286d52 100644
--- a/get_together/templates/get_together/events/show_series.html
+++ b/get_together/templates/get_together/events/show_series.html
@@ -94,7 +94,7 @@
diff --git a/get_together/templates/get_together/teams/show_team.html b/get_together/templates/get_together/teams/show_team.html
index a60ace1..2d62ab9 100644
--- a/get_together/templates/get_together/teams/show_team.html
+++ b/get_together/templates/get_together/teams/show_team.html
@@ -65,8 +65,8 @@
{% for event in upcoming_events %}
-
-
+
+
{% if event.status == event.CANCELED %}
{% endif %}{{event.name}}{% if event.status == event.CANCELED %} (Canceled){% endif %}
{{ event.place }}
{{ event.local_start_time }}
diff --git a/get_together/urls.py b/get_together/urls.py
index 43d5e27..9c3f908 100644
--- a/get_together/urls.py
+++ b/get_together/urls.py
@@ -85,6 +85,8 @@ urlpatterns = [
path('events/
/+sponsor/', views.sponsor_event, name='sponsor-event'),
path('events//+invite/', views.invite_attendees, name='invite-attendees'),
path('events//+delete/', views.delete_event, name='delete-event'),
+ path('events//+cancel/', views.cancel_event, name='cancel-event'),
+ path('events//+restore/', views.restore_event, name='restore-event'),
path('events//+add_place/', views.add_place_to_event, name='add-place'),
path('events//+comment/', views.comment_event, name='comment-event'),
path('events//+photo/', views.add_event_photo, name='add-event-photo'),
diff --git a/get_together/views/events.py b/get_together/views/events.py
index be075c7..39d9958 100644
--- a/get_together/views/events.py
+++ b/get_together/views/events.py
@@ -30,6 +30,7 @@ from events.forms import (
TeamEventForm,
NewTeamEventForm,
DeleteEventForm,
+ CancelEventForm,
EventSeriesForm,
DeleteEventSeriesForm,
EventCommentForm,
@@ -54,7 +55,7 @@ import simplejson
def events_list(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('all-events')
- events = Event.objects.filter(attendees=request.user.profile, end_time__gt=timezone.now()).order_by('start_time')
+ events = Event.objects.filter(attendees=request.user.profile, end_time__gt=timezone.now(), status__gt=Event.CANCELED).order_by('start_time')
geo_ip = location.get_geoip(request)
context = {
'active': 'my',
@@ -430,6 +431,10 @@ def attend_event(request, event_id):
messages.add_message(request, messages.WARNING, message=_("You can not change your status on an event that has ended."))
return redirect(event.get_absolute_url())
+ if event.status == event.CANCELED:
+ messages.add_message(request, messages.WARNING, message=_("This event has been canceled."))
+ return redirect(event.get_absolute_url())
+
try:
attendee = Attendee.objects.get(event=event, user=request.user.profile)
except:
@@ -687,6 +692,80 @@ def delete_event(request, event_id):
else:
return redirect('home')
+@login_required
+def cancel_event(request, event_id):
+ event = get_object_or_404(Event, id=event_id)
+ if not request.user.profile.can_edit_event(event):
+ messages.add_message(request, messages.WARNING, message=_('You can not make changes to this event.'))
+ return redirect(event.get_absolute_url())
+
+ if request.method == 'GET':
+ form = CancelEventForm()
+
+ context = {
+ 'team': event.team,
+ 'event': event,
+ 'cancel_form': form,
+ }
+ return render(request, 'get_together/events/cancel_event.html', context)
+ elif request.method == 'POST':
+ form = CancelEventForm(request.POST)
+ if form.is_valid() and form.cleaned_data['confirm']:
+ event.status = Event.CANCELED
+ event.save()
+ send_cancellation_emails(event, form.cleaned_data['reason'], request.user)
+ return redirect(event.get_absolute_url())
+ else:
+ context = {
+ 'team': event.team,
+ 'event': event,
+ 'cancel_form': form,
+ }
+ return render(request, 'get_together/events/cancel_event.html', context)
+ else:
+ return redirect('home')
+
+def send_cancellation_emails(event, reason, canceled_by):
+ context = {
+ 'event': event,
+ 'reason': reason,
+ 'by': canceled_by.profile,
+ 'site': Site.objects.get(id=1),
+ }
+ email_subject = 'Event canceled: %s' % event.name
+ email_body_text = render_to_string('get_together/emails/events/event_canceled.txt', context)
+ email_body_html = render_to_string('get_together/emails/events/event_canceled.html', context)
+ email_from = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@gettogether.community')
+
+ for attendee in event.attendees.filter(user__account__is_email_confirmed=True):
+ success = send_mail(
+ from_email=email_from,
+ html_message=email_body_html,
+ message=email_body_text,
+ recipient_list=[attendee.user.email],
+ subject=email_subject,
+ fail_silently=True,
+ )
+ EmailRecord.objects.create(
+ sender=canceled_by,
+ recipient=attendee.user,
+ email=attendee.user.email,
+ subject=email_subject,
+ body=email_body_text,
+ ok=success
+ )
+
+@login_required
+def restore_event(request, event_id):
+ event = get_object_or_404(Event, id=event_id)
+ if not request.user.profile.can_edit_event(event):
+ messages.add_message(request, messages.WARNING, message=_('You can not make changes to this event.'))
+ return redirect(event.get_absolute_url())
+
+ event.status = Event.CONFIRMED
+ event.save()
+ return redirect(event.get_absolute_url())
+
@login_required
def edit_series(request, series_id):
series = get_object_or_404(EventSeries, id=series_id)