Allow cancelling an event with a reason, notify attendees of the change. Fixed #91

This commit is contained in:
Michael Hall 2018-06-23 12:04:45 -04:00
parent 4b0acb0794
commit 316a047f14
12 changed files with 178 additions and 6 deletions

View file

@ -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=_(""))

View file

@ -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),
),
]

View file

@ -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)

View file

@ -0,0 +1,14 @@
{% extends "get_together/emails/base.html" %}
{% block content %}
<h3>Event canceled: {{event.name|striptags}}</h3>
<p><b>Date:</b> {{event.local_start_time}}</p>
<p><b>Canceled by:</b> {{by}}</p>
<p><b>Reason:</b> {{reason}}</p>
<a href="{{event.get_full_url}}" title="{{ event.name|striptags }} page.">View this event.</a>
</p>
{% endblock %}

View file

@ -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 %}

View file

@ -0,0 +1,15 @@
{% extends "get_together/base.html" %}
{% block content %}
<h2>Confirm deletion</h2>
Are you sure you want to cancel <strong>{{event.name}}</strong>?
<form action="{% url "cancel-event" event.id %}" method="post">
{% csrf_token %}
<div class="form-group">
{{ cancel_form.as_p }}
<br />
<button type="submit" class="btn btn-danger">Cancel Event</button>
</div>
</form>
{% endblock %}

View file

@ -15,6 +15,11 @@
<br />
<button type="submit" class="btn btn-primary">Save</button>
</form>
{% if event.status == event.CANCELED %}
<a href="{% url 'restore-event' event.id %}" class="btn btn-success">Restore</a>
{% else %}
<a href="{% url 'cancel-event' event.id %}" class="btn btn-secondary">Cancel</a>
{% endif %}
<a href="{% url 'delete-event' event.id %}" class="btn btn-danger">Delete</a>
{% endblock %}

View file

@ -102,12 +102,13 @@
{% if team.banner_img %}
<div class="team-banner">
<img class="card-img-top" src="{{ team.banner_img.url }}" alt="{{team.name}}'s cover image" height="200px" width="825px">
<h2 class="team-title">{{ event.name }}</h2>
<h2 class="team-title">{{ event.name }}{% if event.status == event.CANCELED %} (Canceled){% endif %}</h2>
</div>
{% else %}
<h2>{{ event.name }}</h2>
{% endif %}
<p class="text-muted">Hosted by <a href="{% url "show-team-by-slug" team.slug %}">{{ team.name }}</a></p>
{% if event.status != event.CANCELED %}
{% if settings.SOCIAL_AUTH_TWITTER_KEY %}
<a href="https://twitter.com/intent/tweet?text=I'm+having+a+get+together!%0D{{event.name|urlencode}}&original_referer={{event.get_full_url|urlencode}}&url={{event.get_full_url|urlencode}}&hashtags=gettogether" data-size="large" class="btn btn-twitter btn-sm"><i class="fa fa-twitter"></i> Tweet</a>
{% endif %}
@ -134,6 +135,7 @@
</div>
{% endif %}
{% endif %}
{% endif %}
{% if can_edit_event %}
<div class="btn-group dropdown">
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="editMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -148,6 +150,12 @@
</div>
{% endif %}
{% if event.status == event.CANCELED %}
<p class="alerts">
<div class="alert alert-danger">This event has been canceled.</div>
</p>
{% endif %}
<p>{{ event.summary|markdown }}</p>
<table class="table">

View file

@ -94,7 +94,7 @@
<div class="col media gt-profile">
<div class="media-body">
<h6 class="mt-2 mb-0">
<a href="{{ instance.get_absolute_url }}">{{instance.local_start_time.date}}</a>
{% if event.status == event.CANCELED %}<del>{% endif %}<a href="{{ instance.get_absolute_url }}">{{instance.local_start_time.date}}</a>{% if event.status == event.CANCELED %}</del> (Canceled){% endif %}
<br/><small class="text-muted">{{ instance.name }}</small>
</div>
</div>

View file

@ -65,8 +65,8 @@
</h4>
<div class="container">
{% for event in upcoming_events %}
<div class="row">
<div class="col"><a href="{{ event.get_absolute_url }}">{{event.name}}</a></div>
<div class="row{% if event.status == event.CANCELED %} text-muted{% endif %}">
<div class="col">{% if event.status == event.CANCELED %}<del>{% endif %}<a href="{{ event.get_absolute_url }}">{{event.name}}</a>{% if event.status == event.CANCELED %}</del> (Canceled){% endif %}</div>
<div class="col">{{ event.place }}</div>
<div class="col">{{ event.local_start_time }}</div>
</div>

View file

@ -85,6 +85,8 @@ urlpatterns = [
path('events/<int:event_id>/+sponsor/', views.sponsor_event, name='sponsor-event'),
path('events/<int:event_id>/+invite/', views.invite_attendees, name='invite-attendees'),
path('events/<int:event_id>/+delete/', views.delete_event, name='delete-event'),
path('events/<int:event_id>/+cancel/', views.cancel_event, name='cancel-event'),
path('events/<int:event_id>/+restore/', views.restore_event, name='restore-event'),
path('events/<int:event_id>/+add_place/', views.add_place_to_event, name='add-place'),
path('events/<int:event_id>/+comment/', views.comment_event, name='comment-event'),
path('events/<int:event_id>/+photo/', views.add_event_photo, name='add-event-photo'),

View file

@ -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)