Add ability for an event host to view and contact attendees. Add ability for an event host to mark if an attendee actually attended or not after the event is over. Fixes #71

This commit is contained in:
Michael Hall 2018-05-16 21:30:32 -04:00
parent c2a2bef699
commit 83ef285f8f
9 changed files with 327 additions and 3 deletions

View file

@ -265,6 +265,10 @@ class EventInviteMemberForm(forms.Form):
class EventInviteEmailForm(forms.Form):
emails = MultiEmailField(label=_(""), widget=forms.widgets.Textarea)
class EventContactForm(forms.Form):
to = forms.ChoiceField(label=_(""))
body = forms.CharField(label=_(""), widget=forms.widgets.Textarea)
class EventSeriesForm(forms.ModelForm):
class Meta:
model = EventSeries

View file

@ -0,0 +1,23 @@
# Generated by Django 2.0 on 2018-05-17 00:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0029_add_team_premium_fields'),
]
operations = [
migrations.AddField(
model_name='attendee',
name='actual',
field=models.SmallIntegerField(choices=[(-1, 'No'), (0, 'Maybe'), (1, 'Yes')], db_index=True, default=0, verbose_name='Attended'),
),
migrations.AlterField(
model_name='attendee',
name='status',
field=models.SmallIntegerField(choices=[(-1, 'No'), (0, 'Maybe'), (1, 'Yes')], db_index=True, default=1, verbose_name='Attending'),
),
]

View file

@ -77,6 +77,10 @@ class Event(models.Model):
attendees = models.ManyToManyField(UserProfile, through='Attendee', related_name="attending", blank=True)
@property
def is_over(self):
return self.end_time <= timezone.now()
@property
def tz(self):
if self.place is not None:
@ -210,7 +214,8 @@ class Attendee(models.Model):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
role = models.SmallIntegerField(_("Role"), choices=ROLES, default=NORMAL, db_index=True)
status = models.SmallIntegerField(_("Attending?"), choices=STATUSES, default=YES, db_index=True)
status = models.SmallIntegerField(_("Attending"), choices=STATUSES, default=YES, db_index=True)
actual = models.SmallIntegerField(_("Attended"), choices=STATUSES, default=MAYBE, db_index=True)
joined_date = models.DateTimeField(default=timezone.now)
last_reminded = models.DateTimeField(null=True, blank=True)
@ -222,6 +227,10 @@ class Attendee(models.Model):
def status_name(self):
return Attendee.STATUSES[self.status+1][1]
@property
def actual_name(self):
return Attendee.STATUSES[self.actual+1][1]
def __str__(self):
return "%s at %s" % (self.user, self.event)

View file

@ -0,0 +1,13 @@
{% extends "get_together/emails/base.html" %}
{% block content %}
<h3>Message about {{event.name|striptags}}</h3>
<p><strong>Sender</strong>: {{ sender|striptags }}<br></p>
<p>{{body|striptags}}</p>
<br>
<a href="{{event.get_full_url}}" title="{{ event.name|striptags }} page.">Go to the event page.</a>
</p>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends 'get_together/emails/base.txt' %}
{% block content %}
== Message about {{event.name}} ==
Sender: {{ sender }}
{{ body|striptags }}
Event page: {{event.get_full_url}}
{% endblock %}

View file

@ -0,0 +1,130 @@
{% extends "get_together/base.html" %}
{% load static markup tz %}
{% block add_to_title %} | {{team.name}}{% endblock %}
{% block styles %}
<link href="{% static 'css/bootstrap-album.css' %}" rel="stylesheet"/>
<style>
.gt-profile {
text-align: center;
}
.gt-profile .gt-profile-badge {
position: absolute;
top: 6px;
right: 6px;
}
</style>
{% endblock %}
{% block content %}
<div class="fluid-container">
<div class="row">
<div class="col-sm-9">
<h2>{{attendees.count}} Attendees for <a href="{{event.get_absolute_url}}">{{ event.name }}</a>
</h2>
<p><a href="{% url 'invite-attendees' event.id %}" class="btn btn-secondary btn-sm"><i class="fa fa-user-plus"></i> Invite Attendees</a></p>
<div class="container">
<div class="row">
{% for attendee in attendees %}
<div class="col-md-3 gt-profile">
<div class="card mb-1 box-shadow">
<div class="card-banner align-items-center">
<a class="card-link" href="{% url 'show-profile' attendee.user.id %}">
<img class="gt-profile-avatar rounded-circle mt-2" src="{{attendee.user.avatar_url}}" width="128px" height="128px">
</a>
{% if event.is_over and attendee.actual != attendee.MAYBE %}
{% if attendee.actual == attendee.YES %}
<span id="attendee-badge-{{attendee.id}}" class="badge badge-success align-top gt-profile-badge" onClick="show_attended_form({{attendee.id}})">{{ attendee.actual_name }}</span>
{% elif attendee.actual == attendee.NO %}
<span id="attendee-badge-{{attendee.id}}" class="badge badge-danger align-top gt-profile-badge" onClick="show_attended_form({{attendee.id}})">{{ attendee.actual_name }}</span>
{% endif %}
{% else %}
{% if attendee.status == attendee.YES %}
<span id="attendee-badge-{{attendee.id}}" class="badge badge-success align-top gt-profile-badge" onClick="show_attended_form({{attendee.id}})">{{ attendee.status_name }}</span>
{% elif attendee.status == attendee.NO %}
<span id="attendee-badge-{{attendee.id}}" class="badge badge-danger align-top gt-profile-badge" onClick="show_attended_form({{attendee.id}})">{{ attendee.status_name }}</span>
{% elif attendee.status == attendee.MAYBE %}
<span id="attendee-badge-{{attendee.id}}" class="badge badge-default align-top gt-profile-badge" onClick="show_attended_form({{attendee.id}})">{{ attendee.status_name }}</span>
{% endif %}
{% endif %}
</div>
<div class="card-body">
<div class="card-text">
<strong><a class="card-link" href="{% url 'show-profile' attendee.user.id %}">{{attendee.user}}</a></strong>
{% if attendee.user.user.account.is_email_confirmed %}
<a href="javascript:contact_attendee({{attendee.id}});" class="text-muted fa fa-envelope" title="Contact"></a>
{% endif %}
</div>
{% if event.is_over %}
<div id="attendee-actual-form-{{attendee.id}}" class="align-items-center" style="display: {% if attendee.actual == attendee.MAYBE %}block{% else %}none{% endif %}">
<small class="text-muted">Attended:</small><br/>
<div class="btn-group">
<a class="btn btn-success" href="javascript:mark_attended({{attendee.id}}, 'yes');">Yes</a></span>
<a class="btn btn-danger" href="javascript:mark_attended({{attendee.id}}, 'no');">No</a></span>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<hr/>
</div>
<div class="col-sm-3">
<div class="container">
<h4>Contact</h4><hr/>
<div class="row">
<div class="col">
<form action="{% url 'manage-attendees' event.id %}" method="POST">
{% csrf_token %}
{{ contact_form.as_p }}
<button type="submit" class="btn btn-primary btn-sm">Send</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script type="text/javascript">
$(document).ready(function(){
$("#id_to").selectmenu();
});
function contact_attendee(attendee_id) {
$("#id_to").val(attendee_id);
$("#id_to").selectmenu("refresh");
$("#id_body").focus();
}
function mark_attended(attendee_id, value) {
$.getJSON("{% url 'attended-event' event.id %}?attendee="+attendee_id+"&response="+value, function(data, status) {
console.log(data)
if (data.status == "OK") {
var badge = $("#attendee-badge-"+attendee_id)
badge[0].innerText = data.actual
if (data.actual == "Yes") {
badge.removeClass('badge-danger').removeClass('badge-default').addClass('badge-success')
} else if (data.actual == "No") {
badge.removeClass('badge-success').removeClass('badge-default').addClass('badge-danger')
}
$("#attendee-actual-form-"+attendee_id).hide()
}
});
}
function show_attended_form(attendee_id) {
$("#attendee-actual-form-"+attendee_id).show()
}
</script>
{% endblock %}

View file

@ -114,7 +114,7 @@
{% if settings.SOCIAL_AUTH_LINKEDIN_KEY %}
<a href="#" onClick="shareLinkedIn();" class="IN-widget btn btn-linkedin btn-sm"><i class="fa fa-linkedin"></i> Share</a>
{% endif %}
{% if not is_attending %}
{% if not event.is_over and not is_attending %}
<div class="btn-group">
<a href="{% url 'attend-event' event.id %}" class="btn btn-success btn-sm"><i class="fa fa-check-square-o"></i> Attend</a>
<button type="button" class="btn btn-success btn-sm dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -134,6 +134,7 @@
<div class="dropdown-menu" aria-labelledby="editMenuButton">
<a href="{% url 'edit-event' event.id %}" class="dropdown-item">Event Details</a>
<a href="{% url 'schedule-event-talks' event.id %}" class="dropdown-item">Manage Talks</a>
<a href="{% url 'manage-attendees' event.id %}" class="dropdown-item">Manage Attendees</a>
</div>
</div>
{% endif %}
@ -183,7 +184,9 @@
{% for presentation in presentation_list %}
<div><a href="{% url 'show-talk' presentation.talk.id %}">{{presentation.talk.title}}</a> by <a href="{% url 'show-speaker' presentation.talk.speaker.id %}">{{presentation.talk.speaker.user}}, {{presentation.talk.speaker.title}}</a></div>
{% endfor %}
{% if not event.is_over %}
<a class="btn btn-primary btn-sm" href="{% url 'propose-event-talk' event.id %}">Propose a talk</a>
{% endif %}
{% if pending_presentations and can_edit_event %}
<a class="btn btn-success btn-sm" href="{% url 'schedule-event-talks' event.id %}">{{pending_presentations}} proposed talks</a>
{% endif %}
@ -250,7 +253,7 @@
<div class="media-body">
<h6 class="mt-2 mb-0">
<a href="{% url 'show-profile' attendee.user.id %}" title="{{attendee.user}}'s profile">{{attendee.user}}</a>
{% if attendee.user.user == request.user %}
{% if attendee.user.user == request.user and not event.is_over %}
{% if attendee.status == attendee.YES %}
<span class="badge badge-success dropdown-toggle align-top" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ attendee.status_name }}</span>
<div class="dropdown-menu">

View file

@ -79,6 +79,8 @@ urlpatterns = [
path('team/<int:team_id>/+create-event/', views.create_event, name='create-event'),
path('events/<int:event_id>/+edit/', views.edit_event, name='edit-event'),
path('events/<int:event_id>/+attend/', views.attend_event, name='attend-event'),
path('events/<int:event_id>/+attended/', views.attended_event, name='attended-event'),
path('events/<int:event_id>/+attendees/', views.manage_attendees, name='manage-attendees'),
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>/+add_place/', views.add_place_to_event, name='add-place'),

View file

@ -9,6 +9,7 @@ from django.utils import timezone
from django.core.mail import send_mail
from django.template.loader import get_template, render_to_string
from django.conf import settings
from django.http import JsonResponse
from events.models.events import (
Event,
@ -35,6 +36,7 @@ from events.forms import (
NewCommonEventForm,
EventInviteEmailForm,
EventInviteMemberForm,
EventContactForm,
)
from events import location
@ -148,6 +150,80 @@ def create_event(request, team_id):
return redirect('home')
@login_required
def manage_attendees(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 manage this event\'s attendees.'))
return redirect(event.get_absolute_url())
attendees = Attendee.objects.filter(event=event).order_by('-actual', '-status', 'user__realname')
attendee_choices = [(attendee.id, attendee.user) for attendee in attendees if attendee.user.user.account.is_email_confirmed]
default_choices = [('all', 'Everyone (%s)' % len(attendee_choices)), ('hosts', 'Only Hosts')]
if event.is_over:
default_choices.append(('attended', 'Only Attended'))
else:
default_choices.append(('attending', 'Only Attending'))
if request.method == 'POST':
contact_form = EventContactForm(request.POST)
contact_form.fields['to'].choices = default_choices + attendee_choices
if contact_form.is_valid():
to = contact_form.cleaned_data['to']
body = contact_form.cleaned_data['body']
if to is not 'hosts' and not request.user.profile.can_edit_event(event):
messages.add_message(request, messages.WARNING, message=_('You can not contact this events\'s attendees.'))
return redirect(event.get_absolute_url())
if to == 'all':
count = 0
for attendee in Attendee.objects.filter(event=event):
if attendee.user.user.account.is_email_confirmed:
contact_attendee(attendee, body, request.user.profile)
count += 1
messages.add_message(request, messages.SUCCESS, message=_('Emailed %s attendees' % count))
elif to == 'hosts':
count = 0
for attendee in Attendee.objects.filter(event=event, role=Attendee.HOST):
if attendee.user.user.account.is_email_confirmed:
contact_attendee(attendee, body, request.user.profile)
count += 1
messages.add_message(request, messages.SUCCESS, message=_('Emailed %s attendees' % count))
elif to == 'attending':
count = 0
for attendee in Attendee.objects.filter(event=event, status=Attendee.YES):
if attendee.user.user.account.is_email_confirmed:
contact_attendee(attendee, body, request.user.profile)
count += 1
messages.add_message(request, messages.SUCCESS, message=_('Emailed %s attendees' % count))
elif to == 'attended':
count = 0
for attendee in Attendee.objects.filter(event=event, actual=Attendee.YES):
if attendee.user.user.account.is_email_confirmed:
contact_attendee(attendee, body, request.user.profile)
count += 1
messages.add_message(request, messages.SUCCESS, message=_('Emailed %s attendees' % count))
else:
try:
attendee = Attendee.objects.get(id=to)
contact_attendee(attendee, body, request.user.profile)
messages.add_message(request, messages.SUCCESS, message=_('Emailed %s' % attendee.user))
except Member.DoesNotExist:
messages.add_message(request, messages.ERROR, message=_('Error sending message: Unknown user (%s)'%to))
pass
return redirect('manage-attendees', event.id)
else:
messages.add_message(request, messages.ERROR, message=_('Error sending message: %s' % contact_form.errors))
else:
contact_form = EventContactForm()
contact_form.fields['to'].choices = default_choices + attendee_choices
context = {
'event': event,
'attendees': attendees,
'contact_form': contact_form,
'can_edit_event': request.user.profile.can_edit_event(event),
}
return render(request, 'get_together/events/manage_attendees.html', context)
@login_required
def invite_attendees(request, event_id):
event = get_object_or_404(Event, id=event_id)
@ -242,12 +318,47 @@ def invite_attendee(email, event, sender):
)
def contact_attendee(attendee, body, sender):
context = {
'sender': sender,
'event': attendee.event,
'body': body,
'site': Site.objects.get(id=1),
}
email_subject = '[GetTogether] Message about %s' % attendee.event.name
email_body_text = render_to_string('get_together/emails/attendee_contact.txt', context)
email_body_html = render_to_string('get_together/emails/attendee_contact.html', context)
email_recipients = [attendee.user.user.email]
email_from = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@gettogether.community')
success = send_mail(
from_email=email_from,
html_message=email_body_html,
message=email_body_text,
recipient_list=email_recipients,
subject=email_subject,
fail_silently=True,
)
EmailRecord.objects.create(
sender=sender.user,
recipient=attendee.user.user,
email=attendee.user.user.email,
subject=email_subject,
body=email_body_text,
ok=success
)
def attend_event(request, event_id):
event = Event.objects.get(id=event_id)
if request.user.is_anonymous:
messages.add_message(request, messages.WARNING, message=_("You must be logged in to say you're attending."))
return redirect(event.get_absolute_url())
if event.is_over:
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())
try:
attendee = Attendee.objects.get(event=event, user=request.user.profile)
except:
@ -265,6 +376,24 @@ def attend_event(request, event_id):
return redirect(event.get_absolute_url())
def attended_event(request, event_id):
event = get_object_or_404(Event, id=event_id)
attendee = get_object_or_404(Attendee, id=request.GET.get('attendee', None))
if request.user.is_anonymous:
return JsonResponse({'status': 'ERROR', 'message': _("You must be logged in mark an attendee's actual status.")})
if not event.is_over:
return JsonResponse({'status': 'ERROR', 'message': _("You can not set an attendee's actual status until the event is over")})
if request.GET.get('response', None) == 'yes':
attendee.actual = Attendee.YES
if request.GET.get('response', None) == 'no':
attendee.actual = Attendee.NO
attendee.save()
return JsonResponse({'status': 'OK', 'attendee_id': attendee.id, 'actual': attendee.actual_name})
def comment_event(request, event_id):
event = Event.objects.get(id=event_id)
if request.user.is_anonymous: