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:
parent
c2a2bef699
commit
83ef285f8f
9 changed files with 327 additions and 3 deletions
|
@ -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
|
||||
|
|
23
events/migrations/0030_add_attendee_actual_status.py
Normal file
23
events/migrations/0030_add_attendee_actual_status.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
130
get_together/templates/get_together/events/manage_attendees.html
Normal file
130
get_together/templates/get_together/events/manage_attendees.html
Normal 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 %}
|
|
@ -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">
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue