From 1f5377f38ddcbdd845c94284c4b1ecebf529d786 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89lie=20Bouttier?=
Date: Sat, 4 Nov 2017 15:30:00 +0100
Subject: [PATCH] major overhaul of proposal process
---
cfp/decorators.py | 17 +-
cfp/forms.py | 70 +-
cfp/migrations/0018_auto_20171104_1227.py | 25 +
cfp/models.py | 11 +-
cfp/templates/cfp/closed.html | 15 -
cfp/templates/cfp/complete.html | 27 -
cfp/templates/cfp/proposal_dashboard.html | 77 +++
cfp/templates/cfp/proposal_home.html | 33 +
cfp/templates/cfp/proposal_mail_token.html | 30 +
cfp/templates/cfp/proposal_speaker_form.html | 42 ++
cfp/templates/cfp/proposal_talk_details.html | 86 +++
.../{propose.html => proposal_talk_form.html} | 12 +-
cfp/templates/cfp/speaker.html | 26 -
cfp/templatetags/cfp_tags.py | 5 +
cfp/urls.py | 19 +-
cfp/views.py | 319 ++++++---
locale/fr/LC_MESSAGES/django.mo | Bin 17631 -> 21304 bytes
locale/fr/LC_MESSAGES/django.po | 604 ++++++++++++------
ponyconf/templates/base.html | 2 +-
19 files changed, 1022 insertions(+), 398 deletions(-)
create mode 100644 cfp/migrations/0018_auto_20171104_1227.py
delete mode 100644 cfp/templates/cfp/closed.html
delete mode 100644 cfp/templates/cfp/complete.html
create mode 100644 cfp/templates/cfp/proposal_dashboard.html
create mode 100644 cfp/templates/cfp/proposal_home.html
create mode 100644 cfp/templates/cfp/proposal_mail_token.html
create mode 100644 cfp/templates/cfp/proposal_speaker_form.html
create mode 100644 cfp/templates/cfp/proposal_talk_details.html
rename cfp/templates/cfp/{propose.html => proposal_talk_form.html} (61%)
delete mode 100644 cfp/templates/cfp/speaker.html
diff --git a/cfp/decorators.py b/cfp/decorators.py
index a5ca0ac..40964bc 100644
--- a/cfp/decorators.py
+++ b/cfp/decorators.py
@@ -1,9 +1,22 @@
-from functools import wraps
-
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+
+from functools import wraps
from cfp.utils import is_staff
+from cfp.models import Participant
+
+
+def speaker_required(view_func):
+ def wrapped_view(request, **kwargs):
+ speaker_token = kwargs.pop('speaker_token')
+ # TODO v3: if no speaker token is provided, we should check for a logged user, and if so,
+ # we should check if his/her participating at current conference
+ speaker = get_object_or_404(Participant, site=request.conference.site, token=speaker_token)
+ kwargs['speaker'] = speaker
+ return view_func(request, **kwargs)
+ return wraps(view_func)(wrapped_view)
def staff_required(view_func):
diff --git a/cfp/forms.py b/cfp/forms.py
index b0bef9e..a2bc667 100644
--- a/cfp/forms.py
+++ b/cfp/forms.py
@@ -36,6 +36,27 @@ CONFIRMATION_VALUES = [
]
+class OnSiteNamedModelForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ self.conference = kwargs.pop('conference')
+ super().__init__(*args, **kwargs)
+
+ # we should manually check (site, name) uniqueness as the site is not part of the form
+ def clean_name(self):
+ name = self.cleaned_data['name']
+ if (not self.instance or self.instance.name != name) \
+ and self._meta.model.objects.filter(site=self.conference.site, name=name).exists():
+ raise self.instance.unique_error_message(self._meta.model, ['name'])
+ return name
+
+ def save(self, commit=True):
+ obj = super().save(commit=False)
+ obj.site = self.conference.site
+ if commit:
+ obj.save()
+ return obj
+
+
class VolunteerFilterForm(forms.Form):
activity = forms.MultipleChoiceField(
label=_('Activity'),
@@ -174,15 +195,29 @@ class TalkActionForm(forms.Form):
self.fields['room'].choices = [(None, "---------")] + list(rooms.values_list('slug', 'name'))
-ParticipantForm = modelform_factory(Participant, fields=('name', 'email', 'biography'))
+class ParticipantForm(OnSiteNamedModelForm):
+ def __init__(self, *args, **kwargs):
+ social = kwargs.pop('social', True)
+ super().__init__(*args, **kwargs)
+ if not social:
+ for field in ['twitter', 'linkedin', 'github', 'website', 'facebook', 'mastodon']:
+ self.fields.pop(field)
+
+ class Meta:
+ model = Participant
+ fields = ['name', 'email', 'biography', 'twitter', 'linkedin', 'github', 'website', 'facebook', 'mastodon']
+
+ def clean_email(self):
+ email = self.cleaned_data['email']
+ if (not self.instance or self.instance.email != email) \
+ and self._meta.model.objects.filter(site=self.conference.site, email=email).exists():
+ raise self.instance.unique_error_message(self._meta.model, ['email'])
+ return email
class ParticipantStaffForm(ParticipantForm):
class Meta(ParticipantForm.Meta):
- fields = ('name', 'vip', 'email', 'biography')
- labels = {
- 'name': _('Name'),
- }
+ fields = ['name', 'vip', 'email', 'phone_number', 'notes'] + ParticipantForm.Meta.fields[3:]
class ParticipantFilterForm(forms.Form):
@@ -220,6 +255,10 @@ class ParticipantFilterForm(forms.Form):
self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name'))
+class MailForm(forms.Form):
+ email = forms.EmailField(required=True, label=_('Email'))
+
+
class UsersWidget(ModelSelect2MultipleWidget):
model = User
search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ]
@@ -273,27 +312,6 @@ class CreateUserForm(forms.ModelForm):
return user
-class OnSiteNamedModelForm(forms.ModelForm):
- def __init__(self, *args, **kwargs):
- self.conference = kwargs.pop('conference')
- super().__init__(*args, **kwargs)
-
- # we should manually check (site, name) uniqueness as the site is not part of the form
- def clean_name(self):
- name = self.cleaned_data['name']
- if (not self.instance or self.instance.name != name) \
- and self._meta.model.objects.filter(site=self.conference.site, name=name).exists():
- raise self.instance.unique_error_message(self._meta.model, ['name'])
- return name
-
- def save(self, commit=True):
- obj = super().save(commit=False)
- obj.site = self.conference.site
- if commit:
- obj.save()
- return obj
-
-
class TrackForm(OnSiteNamedModelForm):
class Meta:
model = Track
diff --git a/cfp/migrations/0018_auto_20171104_1227.py b/cfp/migrations/0018_auto_20171104_1227.py
new file mode 100644
index 0000000..7e9884b
--- /dev/null
+++ b/cfp/migrations/0018_auto_20171104_1227.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.5 on 2017-11-04 12:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cfp', '0017_auto_20171103_1922'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='conference',
+ name='acceptances_disclosure_date',
+ field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Acceptances disclosure date'),
+ ),
+ migrations.AlterField(
+ model_name='participant',
+ name='name',
+ field=models.CharField(max_length=128, verbose_name='Name'),
+ ),
+ ]
diff --git a/cfp/models.py b/cfp/models.py
index 09090eb..f9c8ff8 100644
--- a/cfp/models.py
+++ b/cfp/models.py
@@ -34,6 +34,7 @@ class Conference(models.Model):
reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email'))
staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members'))
secure_domain = models.BooleanField(default=True, verbose_name=_('Secure domain (HTTPS)'))
+ acceptances_disclosure_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Acceptances disclosure date'))
schedule_publishing_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Schedule publishing date'))
schedule_redirection_url = models.URLField(blank=True, default='', verbose_name=_('Schedule redirection URL'),
help_text=_('If specified, schedule tab will redirect to this URL.'))
@@ -56,6 +57,11 @@ class Conference(models.Model):
.filter(Q(opening_date__isnull=True) | Q(opening_date__lte=now))\
.filter(Q(closing_date__isnull=True) | Q(closing_date__gte=now))
+ @property
+ def disclosed_acceptances(self):
+ # acceptances are automatically disclosed if the schedule is published
+ return self.acceptances_disclosure_date and self.acceptances_disclosure_date <= timezone.now() or self.schedule_available
+
@property
def schedule_available(self):
return self.schedule_publishing_date and self.schedule_publishing_date <= timezone.now()
@@ -90,7 +96,7 @@ class ParticipantManager(models.Manager):
class Participant(PonyConfModel):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
- name = models.CharField(max_length=128, verbose_name=_('Your Name'))
+ name = models.CharField(max_length=128, verbose_name=_('Name'))
email = models.EmailField()
biography = models.TextField(verbose_name=_('Biography'))
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@@ -110,10 +116,11 @@ class Participant(PonyConfModel):
objects = ParticipantManager()
def get_absolute_url(self):
- return reverse('participant-details', kwargs={'participant_id': self.token})
+ return reverse('proposal-dashboard', kwargs={'speaker_token': self.token})
class Meta:
# A User can participe only once to a Conference (= Site)
+ unique_together = ('site', 'name')
unique_together = ('site', 'email')
def __str__(self):
diff --git a/cfp/templates/cfp/closed.html b/cfp/templates/cfp/closed.html
deleted file mode 100644
index 792703c..0000000
--- a/cfp/templates/cfp/closed.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends 'base.html' %}
-{% load i18n %}
-
-{% block proposetab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-{% trans "Sorry, the Call for Participation is closed!" %}
-
-{% endblock %}
diff --git a/cfp/templates/cfp/complete.html b/cfp/templates/cfp/complete.html
deleted file mode 100644
index c25b4a2..0000000
--- a/cfp/templates/cfp/complete.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends 'base.html' %}
-
-{% load ponyconf_tags i18n %}
-
-{% block proposetab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-
-
-
{% trans "Thanks for your proposal" %} {{ participant }} !
-
{% trans "You can at anytime:" %}
-
-
-
{% trans "An email has been sent to you with those URLs" %}
-
-
-{% endblock %}
diff --git a/cfp/templates/cfp/proposal_dashboard.html b/cfp/templates/cfp/proposal_dashboard.html
new file mode 100644
index 0000000..c1d3b28
--- /dev/null
+++ b/cfp/templates/cfp/proposal_dashboard.html
@@ -0,0 +1,77 @@
+{% extends 'base.html' %}
+{% load i18n crispy_forms_tags cfp_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+{% trans "Your informations" %}
+
+
+
+
+
+{% trans "Biography" %}
+
+
+ {% if speaker.biography %}
+ {{ speaker.biography|linebreaksbr }}
+ {% else %}
+ {% trans "No biography." %}
+ {% endif %}
+
+
+{% trans "Your proposals" %}
+
+
+ {% for talk in talks %}
+ {% if forloop.first %}
+
+ {% endif %}
+ -
+ {{ talk }}
+ {% for spkr in talk.speakers|exclude:speaker %}
+ {% if forloop.first %}{% trans "with" %}{% endif %}
+ {{ spkr }}
+ {% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
+ {% endfor %}
+ {% if conference.disclosed_acceptances and talk.accepted %}
+ {% if talk.confirmed is None %}
+ {% trans "you must confirm you participation" %}
+ {% elif talk.confirmed %}
+ {% trans "accepted" %}
+ {% else %}
+ {% trans "cancelled" %}
+ {% endif %}
+ {% endif %}
+
+ {% if forloop.last %}
+
+ {% endif %}
+ {% empty %}
+ {% trans "No proposals." %}
+ {% endfor %}
+
+
+ {% trans "New proposal" %}
+
+{% endblock %}
diff --git a/cfp/templates/cfp/proposal_home.html b/cfp/templates/cfp/proposal_home.html
new file mode 100644
index 0000000..6622729
--- /dev/null
+++ b/cfp/templates/cfp/proposal_home.html
@@ -0,0 +1,33 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% url 'proposal-mail-token' as mail_token_url %}
+ {% blocktrans %}If you already have submitted a talk and you want to edit it or submit another one, please click
here.{% endblocktrans %}
+
+
+
+
+{% endblock %}
diff --git a/cfp/templates/cfp/proposal_mail_token.html b/cfp/templates/cfp/proposal_mail_token.html
new file mode 100644
index 0000000..9ad8b31
--- /dev/null
+++ b/cfp/templates/cfp/proposal_mail_token.html
@@ -0,0 +1,30 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+
+{% endblock %}
diff --git a/cfp/templates/cfp/proposal_speaker_form.html b/cfp/templates/cfp/proposal_speaker_form.html
new file mode 100644
index 0000000..13e2a82
--- /dev/null
+++ b/cfp/templates/cfp/proposal_speaker_form.html
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/cfp/templates/cfp/proposal_talk_details.html b/cfp/templates/cfp/proposal_talk_details.html
new file mode 100644
index 0000000..9c2878a
--- /dev/null
+++ b/cfp/templates/cfp/proposal_talk_details.html
@@ -0,0 +1,86 @@
+{% extends 'base.html' %}
+{% load i18n crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+{% trans "Status" %}
+
+{% if not conference.disclosed_acceptances or talk.accepted is None %}
+{% trans "Reviewing in progress, we will keep you informed by mail." %}
+{% elif talk.accepted %}
+{% trans "Accepted!" %}
+{% if talk.confirmed is None %}
+
+ {% trans "Please confirm your participation:" %}
+ {% trans "I will be there!" %}
+ {% trans "Sorry, couldn't make it :-(" %}
+
+{% elif talk.confirmed %}
+
+ {% trans "Sorry, I have to cancel." %}
+
+{% else %}
+
+ {% trans "Good news, I finally could be there!" %}
+
+{% endif %}
+{% else %}
+{% trans "Sorry, refused :-(" %}
+{% endif %}
+
+{% trans "Speakers" %}
+
+
+ {% for spkr in talk.speakers.all %}
+ {% if forloop.first %}
{% endif %}
+ -
+ {{ spkr }}
+ {% if spkr.pk == speaker.pk %} ({% trans "you!" %}){% endif %}
+
+ {% if forloop.last %}
{% endif %}
+ {% endfor %}
+
+ {% trans "Add a co-speaker" %}
+
+
+
+{% trans "Description" %}
+
+
+ {% if talk.description %}
+ {{ talk.description|linebreaksbr }}
+ {% else %}
+ {% trans "No description provided." %}
+ {% endif %}
+
+
+{% trans "Message to organizers" %}
+
+
+ {% if talk.notes %}
+ {{ talk.notes|linebreaksbr }}
+ {% else %}
+ {% trans "No description provided." %}
+ {% endif %}
+
+{% endblock %}
diff --git a/cfp/templates/cfp/propose.html b/cfp/templates/cfp/proposal_talk_form.html
similarity index 61%
rename from cfp/templates/cfp/propose.html
rename to cfp/templates/cfp/proposal_talk_form.html
index a59db31..b7ba544 100644
--- a/cfp/templates/cfp/propose.html
+++ b/cfp/templates/cfp/proposal_talk_form.html
@@ -8,7 +8,14 @@
{% block content %}
@@ -16,8 +23,7 @@