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}}? +
+{% csrf_token %} +
+{{ cancel_form.as_p }} +
+ +
+
+{% 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 %}
{{team.name}}'s cover image -

{{ 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 %} Tweet {% 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 @@
- {{instance.local_start_time.date}} + {% if event.status == event.CANCELED %}{% endif %}{{instance.local_start_time.date}}{% if event.status == event.CANCELED %} (Canceled){% endif %}
{{ instance.name }}
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)