Allow cancelling an event with a reason, notify attendees of the change. Fixed #91
This commit is contained in:
parent
4b0acb0794
commit
316a047f14
12 changed files with 178 additions and 6 deletions
|
@ -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=_(""))
|
||||
|
||||
|
|
19
events/migrations/0035_add_event_status.py
Normal file
19
events/migrations/0035_add_event_status.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
15
get_together/templates/get_together/events/cancel_event.html
Normal file
15
get_together/templates/get_together/events/cancel_event.html
Normal 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 %}
|
||||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue