diff --git a/cfp/forms.py b/cfp/forms.py index 125cf1b..d406d3b 100644 --- a/cfp/forms.py +++ b/cfp/forms.py @@ -12,17 +12,28 @@ from django_select2.forms import ModelSelect2MultipleWidget from .models import Participant, Talk, TalkCategory, Track, Conference, Room, Volunteer -STATUS_CHOICES = [ +ACCEPTATION_CHOICES = [ ('pending', _('Pending decision')), ('accepted', _('Accepted')), ('declined', _('Declined')), ] -STATUS_VALUES = [ +ACCEPTATION_VALUES = [ ('pending', None), ('accepted', True), ('declined', False), ] +CONFIRMATION_CHOICES = [ + ('waiting', _('Waiting')), + ('confirmed', _('Confirmed')), + ('desisted', _('Desisted')), +] +CONFIRMATION_VALUES = [ + ('waiting', None), + ('confirmed', True), + ('desisted', False), +] + class TalkForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -69,11 +80,17 @@ class TalkFilterForm(forms.Form): widget=forms.CheckboxSelectMultiple, choices=[], ) - status = forms.MultipleChoiceField( - label=_('Status'), + accepted = forms.MultipleChoiceField( + label=_('Accepted'), required=False, widget=forms.CheckboxSelectMultiple, - choices=STATUS_CHOICES, + choices=ACCEPTATION_CHOICES, + ) + confirmed = forms.MultipleChoiceField( + label=_('Confirmed'), + required=False, + widget=forms.CheckboxSelectMultiple, + choices=CONFIRMATION_CHOICES, ) track = forms.MultipleChoiceField( label=_('Track'), @@ -145,11 +162,17 @@ class ParticipantFilterForm(forms.Form): widget=forms.CheckboxSelectMultiple, choices=[], ) - status = forms.MultipleChoiceField( - label=_('Status'), + accepted = forms.MultipleChoiceField( + label=_('Accepted'), required=False, widget=forms.CheckboxSelectMultiple, - choices=STATUS_CHOICES, + choices=ACCEPTATION_CHOICES, + ) + confirmed = forms.MultipleChoiceField( + label=_('Confirmed'), + required=False, + widget=forms.CheckboxSelectMultiple, + choices=CONFIRMATION_CHOICES, ) track = forms.MultipleChoiceField( label=_('Track'), diff --git a/cfp/migrations/0012_talk_confirmed.py b/cfp/migrations/0012_talk_confirmed.py new file mode 100644 index 0000000..0036220 --- /dev/null +++ b/cfp/migrations/0012_talk_confirmed.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.1 on 2017-10-06 16:36 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cfp', '0011_auto_20171005_2328'), + ] + + operations = [ + migrations.AddField( + model_name='talk', + name='confirmed', + field=models.NullBooleanField(default=None), + ), + ] diff --git a/cfp/models.py b/cfp/models.py index 2f57337..09863b3 100644 --- a/cfp/models.py +++ b/cfp/models.py @@ -243,6 +243,7 @@ class TalkManager(models.Manager): qs = qs.annotate(score=Coalesce(Avg('vote__vote'), 0)) return qs + def talks_materials_destination(talk, filename): return join(talk.site.name, talk.slug, filename) @@ -258,7 +259,6 @@ class Talk(PonyConfModel): ) site = models.ForeignKey(Site, on_delete=models.CASCADE) - speakers = models.ManyToManyField(Participant, verbose_name=_('Speakers')) title = models.CharField(max_length=128, verbose_name=_('Talk Title')) slug = AutoSlugField(populate_from='title', unique=True) @@ -276,6 +276,7 @@ class Talk(PonyConfModel): max_length=10, verbose_name=_("Video licence")) sound = models.BooleanField(_("I need sound"), default=False) accepted = models.NullBooleanField(default=None) + confirmed = models.NullBooleanField(default=None) start_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Beginning date and time')) duration = models.PositiveIntegerField(default=0, verbose_name=_('Duration (min)')) room = models.ForeignKey(Room, blank=True, null=True, default=None) @@ -283,15 +284,11 @@ class Talk(PonyConfModel): materials = models.FileField(null=True, upload_to=talks_materials_destination, verbose_name=_('Materials'), help_text=_('You can use this field to share some materials related to your intervention.')) video = models.URLField(max_length=1000, blank=True, default='', verbose_name='Video URL') - token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) - conversation = models.OneToOneField(MessageThread) - objects = TalkManager() - class Meta: ordering = ('title',) @@ -307,6 +304,32 @@ class Talk(PonyConfModel): else: return ', '.join(speakers[:-1]) + ' & ' + str(speakers[-1]) + def get_status_str(self): + if self.accepted is True: + if self.confirmed is True: + return _('Confirmed') + elif self.confirmed is False: + return _('Cancelled') + else: + return _('Waiting confirmation') + elif self.accepted is False: + return _('Refused') + else: + return _('Pending decision, score: %(score).1f') % {'score': self.score} + + def get_status_color(self): + if self.accepted is True: + if self.confirmed is True: + return 'success' + elif self.confirmed is False: + return 'danger' + else: + return 'info' + elif self.accepted is False: + return 'muted' + else: + return 'warning' + @property def estimated_duration(self): return self.duration or self.category.duration diff --git a/cfp/templates/cfp/staff/participant_list.html b/cfp/templates/cfp/staff/participant_list.html index d2afe24..0efad02 100644 --- a/cfp/templates/cfp/staff/participant_list.html +++ b/cfp/templates/cfp/staff/participant_list.html @@ -15,7 +15,8 @@
{% bootstrap_field filter_form.category layout="horizontal" %} - {% bootstrap_field filter_form.status layout="horizontal" %} + {% bootstrap_field filter_form.accepted layout="horizontal" %} + {% bootstrap_field filter_form.confirmed layout="horizontal" %}
{% bootstrap_field filter_form.track layout="horizontal" %} diff --git a/cfp/templates/cfp/staff/talk_details.html b/cfp/templates/cfp/staff/talk_details.html index 3e8f386..ee50641 100644 --- a/cfp/templates/cfp/staff/talk_details.html +++ b/cfp/templates/cfp/staff/talk_details.html @@ -16,7 +16,7 @@
{{ talk.category }}
{% trans "Status" %}
-
{{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}
+
{{ talk.get_status_str }}
{% trans "Track" %}
{% if talk.track %} diff --git a/cfp/templates/cfp/staff/talk_list.html b/cfp/templates/cfp/staff/talk_list.html index 12eca28..e8f9355 100644 --- a/cfp/templates/cfp/staff/talk_list.html +++ b/cfp/templates/cfp/staff/talk_list.html @@ -14,7 +14,8 @@
- {% bootstrap_field filter_form.status layout="horizontal" %} + {% bootstrap_field filter_form.accepted layout="horizontal" %} + {% bootstrap_field filter_form.confirmed layout="horizontal" %} {% bootstrap_field filter_form.category layout="horizontal" %} {% bootstrap_field filter_form.vote layout="horizontal" %} {% bootstrap_field filter_form.scheduled layout="horizontal" %} @@ -49,7 +50,7 @@ {% if forloop.first %} {% endif %} - + {{ talk.title }} {{ talk.category }} @@ -62,13 +63,7 @@ {{ talk.track|default:"–" }} - {% if talk.accepted == True %} - {% trans "Accepted" %} - {% elif talk.accepted == False %} - {% trans "Declined" %} - {% else %} - {% blocktrans with score=talk.score|floatformat:1 %}Pending, score: {{ score }}{% endblocktrans %} - {% endif %} + {{ talk.get_status_str }} {% if forloop.last%} diff --git a/cfp/urls.py b/cfp/urls.py index e2e2978..bfe7e8b 100644 --- a/cfp/urls.py +++ b/cfp/urls.py @@ -8,6 +8,8 @@ urlpatterns = [ url(r'^cfp/(?P[\w\-]+)/speaker/add/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-add'), url(r'^cfp/(?P[\w\-]+)/speaker/(?P[\w\-]+)/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-edit'), url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/$', views.talk_proposal, name='talk-proposal-edit'), + url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/confirm/$', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm'), + url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/desist/$', views.talk_acknowledgment, {'confirm': False}, name='talk-desist'), url(r'^volunteer/$', views.volunteer_enrole, name='volunteer-enrole'), url(r'^volunteer/(?P[\w\-]+)/$', views.volunteer, name='volunteer'), url(r'^volunteer/(?P[\w\-]+)/join/(?P[\w\-]+)/$', views.volunteer_activity, {'join': True}, name='volunteer-join'), @@ -19,6 +21,8 @@ urlpatterns = [ url(r'^staff/talks/(?P[\w\-]+)/vote/(?P[-+0-2]+)/$', views.talk_vote, name='talk-vote'), url(r'^staff/talks/(?P[\w\-]+)/accept/$', views.talk_decide, {'accept': True}, name='talk-accept'), url(r'^staff/talks/(?P[\w\-]+)/decline/$', views.talk_decide, {'accept': False}, name='talk-decline'), + url(r'^staff/talks/(?P[\w\-]+)/confirm/$', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm-by-staff'), + url(r'^staff/talks/(?P[\w\-]+)/desist/$', views.talk_acknowledgment, {'confirm': False}, name='talk-cancel-by-staff'), url(r'^staff/talks/(?P[\w\-]+)/edit/$', views.TalkUpdate.as_view(), name='talk-edit'), url(r'^staff/speakers/$', views.participant_list, name='participant-list'), url(r'^staff/speakers/(?P[\w\-]+)/$', views.participant_details, name='participant-details'), diff --git a/cfp/views.py b/cfp/views.py index cb44e48..7124b95 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -25,8 +25,8 @@ from .utils import is_staff from .models import Participant, Talk, TalkCategory, Vote, Track, Room, Volunteer, Activity from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, \ ParticipantForm, ParticipantStaffForm, ParticipantFilterForm, \ - ConferenceForm, CreateUserForm, STATUS_VALUES, TrackForm, RoomForm, \ - VolunteerForm + ConferenceForm, CreateUserForm, TrackForm, RoomForm, VolunteerForm, \ + ACCEPTATION_VALUES, CONFIRMATION_VALUES def home(request): @@ -177,6 +177,47 @@ def talk_proposal_speaker_edit(request, talk_id, participant_id=None): }) +def talk_acknowledgment(request, talk_id, confirm, participant_id=None): + # TODO: handle multiple speakers case + talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site) + if participant_id: + participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site) + elif not is_staff(request, request.user): + raise PermissionDenied + else: + participant = None + if not talk.accepted: + raise PermissionDenied + if talk.confirmed != confirm: + talk.confirmed = confirm + talk.save() + if confirm: + confirmation_message= _('Your participation has been taken into account, thank you!') + if participant: + thread_note = _('Speaker %(speaker)s confirmed his/her participation.') + else: + thread_note = _('The talk have been confirmed.') + else: + confirmation_message = _('We have noted your unavailability.') + if participant: + thread_note = _('Speaker %(speaker)s CANCELLED his/her participation.') + else: + thread_note = _('The talk have been cancelled.') + if participant_id: + thread_note = thread_note % {'speaker': participant} + Message.objects.create(thread=talk.conversation, author=participant or request.user, content=thread_note) + messages.success(request, confirmation_message) + else: + if confirm: + messages.warning(request, _('You already confirmed your participation to this talk.')) + else: + messages.warning(request, _('You already cancelled your participation to this talk.')) + if participant: + return redirect(reverse('talk-proposal-edit', kwargs=dict(talk_id=talk_id, participant_id=participant_id))) + else: + return redirect(reverse('talk-details', kwargs=dict(talk_id=talk_id))) + + @staff_required def staff(request): return render(request, 'cfp/staff/base.html') @@ -193,9 +234,13 @@ def talk_list(request): if len(data['category']): show_filters = True talks = talks.filter(reduce(lambda x, y: x | y, [Q(category__pk=pk) for pk in data['category']])) - if len(data['status']): + if len(data['accepted']): show_filters = True - talks = talks.filter(reduce(lambda x, y: x | y, [Q(accepted=dict(STATUS_VALUES)[status]) for status in data['status']])) + talks = talks.filter(reduce(lambda x, y: x | y, [Q(accepted=dict(ACCEPTATION_VALUES)[status]) for status in data['accepted']])) + if len(data['confirmed']): + show_filters = True + talks = talks.filter(accepted=True) + talks = talks.filter(reduce(lambda x, y: x | y, [Q(confirmed=dict(CONFIRMATION_VALUES)[status]) for status in data['confirmed']])) if data['room'] != None: show_filters = True talks = talks.filter(room__isnull=not data['room']) @@ -355,9 +400,13 @@ def participant_list(request): if len(data['category']): show_filters = True talks = talks.filter(reduce(lambda x, y: x | y, [Q(category__pk=pk) for pk in data['category']])) - if len(data['status']): + if len(data['accepted']): show_filters = True - talks = talks.filter(reduce(lambda x, y: x | y, [Q(accepted=dict(STATUS_VALUES)[status]) for status in data['status']])) + talks = talks.filter(reduce(lambda x, y: x | y, [Q(accepted=dict(ACCEPTATION_VALUES)[status]) for status in data['accepted']])) + if len(data['confirmed']): + show_filters = True + talks = talks.filter(accepted=True) + talks = talks.filter(reduce(lambda x, y: x | y, [Q(confirmed=dict(CONFIRMATION_VALUES)[status]) for status in data['confirmed']])) if len(data['track']): show_filters = True q = Q()