major overhaul of proposal process

This commit is contained in:
Élie Bouttier 2017-11-04 15:30:00 +01:00
parent 72f254b3d3
commit 1f5377f38d
19 changed files with 1022 additions and 398 deletions

View File

@ -1,9 +1,22 @@
from functools import wraps
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required 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.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): def staff_required(view_func):

View File

@ -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): class VolunteerFilterForm(forms.Form):
activity = forms.MultipleChoiceField( activity = forms.MultipleChoiceField(
label=_('Activity'), label=_('Activity'),
@ -174,15 +195,29 @@ class TalkActionForm(forms.Form):
self.fields['room'].choices = [(None, "---------")] + list(rooms.values_list('slug', 'name')) 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 ParticipantStaffForm(ParticipantForm):
class Meta(ParticipantForm.Meta): class Meta(ParticipantForm.Meta):
fields = ('name', 'vip', 'email', 'biography') fields = ['name', 'vip', 'email', 'phone_number', 'notes'] + ParticipantForm.Meta.fields[3:]
labels = {
'name': _('Name'),
}
class ParticipantFilterForm(forms.Form): 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')) 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): class UsersWidget(ModelSelect2MultipleWidget):
model = User model = User
search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ] search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ]
@ -273,27 +312,6 @@ class CreateUserForm(forms.ModelForm):
return user 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 TrackForm(OnSiteNamedModelForm):
class Meta: class Meta:
model = Track model = Track

View File

@ -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'),
),
]

View File

@ -34,6 +34,7 @@ class Conference(models.Model):
reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email')) reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email'))
staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members')) staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members'))
secure_domain = models.BooleanField(default=True, verbose_name=_('Secure domain (HTTPS)')) 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_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'), schedule_redirection_url = models.URLField(blank=True, default='', verbose_name=_('Schedule redirection URL'),
help_text=_('If specified, schedule tab will redirect to this 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(opening_date__isnull=True) | Q(opening_date__lte=now))\
.filter(Q(closing_date__isnull=True) | Q(closing_date__gte=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 @property
def schedule_available(self): def schedule_available(self):
return self.schedule_publishing_date and self.schedule_publishing_date <= timezone.now() return self.schedule_publishing_date and self.schedule_publishing_date <= timezone.now()
@ -90,7 +96,7 @@ class ParticipantManager(models.Manager):
class Participant(PonyConfModel): class Participant(PonyConfModel):
site = models.ForeignKey(Site, on_delete=models.CASCADE) 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() email = models.EmailField()
biography = models.TextField(verbose_name=_('Biography')) biography = models.TextField(verbose_name=_('Biography'))
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@ -110,10 +116,11 @@ class Participant(PonyConfModel):
objects = ParticipantManager() objects = ParticipantManager()
def get_absolute_url(self): 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: class Meta:
# A User can participe only once to a Conference (= Site) # A User can participe only once to a Conference (= Site)
unique_together = ('site', 'name')
unique_together = ('site', 'email') unique_together = ('site', 'email')
def __str__(self): def __str__(self):

View File

@ -1,15 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% trans "Participate" %}
</h1>
</div>
<h2>{% trans "Sorry, the Call for Participation is closed!" %}</h2>
{% endblock %}

View File

@ -1,27 +0,0 @@
{% extends 'base.html' %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% trans "Your proposition have been successfully submitted!" %}
</h1>
</div>
<div class="row">
<div class="col-md-12">
<p>{% trans "Thanks for your proposal" %} {{ participant }} !</p>
<p>{% trans "You can at anytime:" %}
<ul>
<li>{% trans "Edit your talk:" %} <a href="{% url 'talk-proposal-edit' talk.token participant.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-edit' talk.token participant.token %}</a></li>
<li>{% trans "Add an additionnal speaker:" %} <a href="{% url 'talk-proposal-speaker-add' talk.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-speaker-add' talk.token %}</a></li>
<li>{% trans "Edit your profile:" %} <a href="{% url 'talk-proposal-speaker-edit' talk.token participant.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-speaker-edit' talk.token participant.token %}</a></li>
</ul>
</p>
<p>{% trans "An email has been sent to you with those URLs" %}</p>
</div>
</div>
{% endblock %}

View File

@ -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 %}
<div class="page-header">
<h1>
{% blocktrans with name=speaker.name %}Welcome <b>{{ name }}</b>!{% endblocktrans %}
<a href="{% url 'proposal-profile-edit' speaker_token=speaker.token %}" class="btn btn-success pull-right">
<span class="glyphicon glyphicon-pencil"></span>&nbsp;{% trans "Edit your profile" %}
</a>
</h1>
</div>
<h3>{% trans "Your informations" %}</h3>
<p>
<ul>
<li><b>{% trans "E-mail:" %}</b> <a href="mailto:{{ speaker.email }}">{{ speaker.email }}</a></li>
{% if speaker.twitter %}<li><b>{% trans "Twitter:" %}</b> <a href="{{ speaker.twitter }}">{{ speaker.twitter }}</a></li>{% endif %}
{% if speaker.linkedin %}<li><b>{% trans "LinkedIn:" %}</b> <a href="{{ speaker.linkedin }}">{{ speaker.linkedin }}</a></li>{% endif %}
{% if speaker.github %}<li><b>{% trans "Github:" %}</b> <a href="{{ speaker.github }}">{{ speaker.github }}</a></li>{% endif %}
{% if speaker.website %}<li><b>{% trans "Website:" %}</b> <a href="{{ speaker.website }}">{{ speaker.website }}</a></li>{% endif %}
{% if speaker.facebook %}<li><b>{% trans "Facebook:" %}</b> <a href="{{ speaker.facebook }}">{{ speaker.facebook }}</a></li>{% endif %}
{% if speaker.mastodon %}<li><b>{% trans "Mastodon:" %}</b> <a href="{{ speaker.mastodon }}">{{ speaker.mastodon }}</a></li>{% endif %}
{% if speaker.phone_number %}<li><b>{% trans "Phone number:" %}</b> {{ speaker.phone_number }}</li>{% endif %}
</ul>
</p>
<h3>{% trans "Biography" %}</h3>
<p>
{% if speaker.biography %}
{{ speaker.biography|linebreaksbr }}
{% else %}
<i>{% trans "No biography." %}</i>
{% endif %}
</p>
<h3>{% trans "Your proposals" %}</h3>
<p>
{% for talk in talks %}
{% if forloop.first %}
<ul>
{% endif %}
<li>
<a href="{% url 'proposal-talk-details' speaker_token=speaker.token talk_id=talk.pk %}"><b>{{ talk }}</b></a>
{% 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 %}
<span class="label label-info">{% trans "you must confirm you participation" %}</span>
{% elif talk.confirmed %}
<span class="label label-success">{% trans "accepted" %}</span>
{% else %}
<span class="label label-danger">{% trans "cancelled" %}</span>
{% endif %}
{% endif %}
</li>
{% if forloop.last %}
</ul>
{% endif %}
{% empty %}
<i>{% trans "No proposals." %}</i>
{% endfor %}
</p>
<p>
<a href="{% url 'proposal-talk-add' speaker_token=speaker.token %}" class="btn btn-primary">{% trans "New proposal" %}</a>
</p>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% trans "Participate" %}
</h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="col-md-8 col-md-offset-2 alert alert-info">
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% 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 <a href="{{ mail_token_url }}">here</a>.{% endblocktrans %}
</div>
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %}
{{ speaker_form|crispy }}
{{ talk_form|crispy }}
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% trans "Access an existing profile" %}
</h1>
</div>
<div class="row">
<div class="col-md-12">
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %}
<div class="form-group">
{% blocktrans %}To receive a email with a link to access your profile, please enter your email below.{% endblocktrans %}
</div>
{{ form|crispy }}
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,42 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% if talk %}
{% if co_speaker %}
{% trans "Edit a speaker" %}
{% else %}
{% trans "Add a co-speaker" %}
<a href="{% url 'proposal-talk-details' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-primary pull-right">
<span class="glyphicon glyphicon-chevron-left"></span>&nbsp;{% trans "Go back to the talk" %}
</a>
{% endif %}
{% else %}
{% trans "Edit your profile" %}
{% comment %}
<a href="{% url 'proposal-dashboard' speaker_token=speaker.token %}" class="btn btn-primary pull-right">
<span class="glyphicon glyphicon-chevron-left"></span>&nbsp;{% trans "My talks" %}
</a>
{% endcomment %}
{% endif %}
</h1>
</div>
<div class="row">
<div class="col-md-12">
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %}
{{ form|crispy }}
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,86 @@
{% extends 'base.html' %}
{% load i18n crispy_forms_tags %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{{ talk.title }}
<div class="pull-right">
<a href="{% url 'proposal-dashboard' speaker_token=speaker.token %}" class="btn btn-primary">
<span class="glyphicon glyphicon-chevron-left"></span>&nbsp;{% trans "My profile" %}
</a>
<a href="{% url 'proposal-talk-add' speaker_token=speaker.token %}" class="btn btn-info">
<span class="glyphicon glyphicon-plus"></span>&nbsp;{% trans "New proposal" %}
</a>
<a href="{% url 'proposal-talk-edit' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-success">
<span class="glyphicon glyphicon-pencil"></span>&nbsp;{% trans "Edit this proposal" %}
</a>
</div>
</h1>
</div>
<h3>{% trans "Status" %}</h3>
{% if not conference.disclosed_acceptances or talk.accepted is None %}
<p>{% trans "Reviewing in progress, we will keep you informed by mail." %}</p>
{% elif talk.accepted %}
<p class="text-success">{% trans "Accepted!" %}</p>
{% if talk.confirmed is None %}
<p>
{% trans "Please confirm your participation:" %}
<a href="{% url 'proposal-talk-confirm' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-success">{% trans "I will be there!" %}</a>
<a href="{% url 'proposal-talk-desist' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-danger">{% trans "Sorry, couldn't make it :-(" %}</a>
</p>
{% elif talk.confirmed %}
<p><a href="{% url 'proposal-talk-desist' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-danger">
{% trans "Sorry, I have to cancel." %}
</a></p>
{% else %}
<p><a href="{% url 'proposal-talk-confirm' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-success">
{% trans "Good news, I finally could be there!" %}
</a></p>
{% endif %}
{% else %}
<p>{% trans "Sorry, refused :-(" %}</p>
{% endif %}
<h3>{% trans "Speakers" %}</h3>
<p>
{% for spkr in talk.speakers.all %}
{% if forloop.first %}<ul>{% endif %}
<li>
<a href="{% url 'proposal-speaker-edit' speaker_token=speaker.token talk_id=talk.pk co_speaker_id=spkr.pk %}">{{ spkr }}</a>
{% if spkr.pk == speaker.pk %} ({% trans "you!" %}){% endif %}
</li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
<a href="{% url 'proposal-speaker-add' speaker_token=speaker.token talk_id=talk.pk %}" class="btn btn-sm btn-success">
<span class="glyphicon glyphicon-plus"></span>&nbsp;{% trans "Add a co-speaker" %}
</a>
</p>
<h3>{% trans "Description" %}</h3>
<p>
{% if talk.description %}
{{ talk.description|linebreaksbr }}
{% else %}
<i>{% trans "No description provided." %}</i>
{% endif %}
</p>
<h3>{% trans "Message to organizers" %}</h3>
<p>
{% if talk.notes %}
{{ talk.notes|linebreaksbr }}
{% else %}
<i>{% trans "No description provided." %}</i>
{% endif %}
</p>
{% endblock %}

View File

@ -8,7 +8,14 @@
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1> <h1>
{% trans "Participate" %} {% if talk %}
{{ talk.title }}
{% else %}
{% trans "Submit a proposal" %}
<a href="{% url 'proposal-dashboard' speaker_token=speaker.token %}" class="btn btn-primary pull-right">
<span class="glyphicon glyphicon-chevron-left"></span>&nbsp;{% trans "Go back to proposals list" %}
</a>
{% endif %}
</h1> </h1>
</div> </div>
@ -16,8 +23,7 @@
<div class="col-md-12"> <div class="col-md-12">
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2"> <form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %} {% csrf_token %}
{{ participant_form|crispy }} {{ form|crispy }}
{{ talk_form|crispy }}
<div class="col-md-12 text-center"> <div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button> <button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
</div> </div>

View File

@ -1,26 +0,0 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load ponyconf_tags i18n %}
{% block proposetab %} class="active"{% endblock %}
{% block content %}
<div class="page-header">
<h1>
{% trans "Participate" %}
</h1>
</div>
<div class="row">
<div class="col-md-12">
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %}
{{ participant_form|crispy }}
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@ -17,3 +17,8 @@ def duration_format(value):
hours = int(value/60) hours = int(value/60)
minutes = value%60 minutes = value%60
return '%d h %02d' % (hours, minutes) return '%d h %02d' % (hours, minutes)
@register.filter
def exclude(queryset, excluded):
return queryset.exclude(pk=excluded.pk)

View File

@ -4,12 +4,27 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^$', views.home, name='home'), url(r'^$', views.home, name='home'),
url(r'^cfp/$', views.talk_proposal, name='talk-proposal'), # v1.1
url(r'^cfp/$', views.proposal_home, name='proposal-home'),
url(r'^cfp/token/$', views.proposal_mail_token, name='proposal-mail-token'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/$', views.proposal_dashboard, name='proposal-dashboard'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/profile/$', views.proposal_speaker_edit, name='proposal-profile-edit'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/add/$', views.proposal_talk_edit, name='proposal-talk-add'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/$', views.proposal_talk_details, name='proposal-talk-details'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/edit/$', views.proposal_talk_edit, name='proposal-talk-edit'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/speaker/add/$', views.proposal_speaker_edit, name='proposal-speaker-add'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/confirm/$', views.proposal_talk_acknowledgment, {'confirm': True}, name='proposal-talk-confirm'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/desist/$', views.proposal_talk_acknowledgment, {'confirm': False}, name='proposal-talk-desist'),
#url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/speaker/(?P<co_speaker_id>[\w\-]+)/$', views.proposal_speaker_details, name='proposal-speaker-details'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/speaker/(?P<co_speaker_id>[\w\-]+)/edit/$', views.proposal_speaker_edit, name='proposal-speaker-edit'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[\w\-]+)/speaker/(?P<co_speaker_id>[\w\-]+)/remove/$', views.proposal_speaker_remove, name='proposal-speaker-remove'),
# Backward compatibility
url(r'^cfp/(?P<talk_id>[\w\-]+)/speaker/add/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-add'), url(r'^cfp/(?P<talk_id>[\w\-]+)/speaker/add/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-add'),
url(r'^cfp/(?P<talk_id>[\w\-]+)/speaker/(?P<participant_id>[\w\-]+)/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-edit'), url(r'^cfp/(?P<talk_id>[\w\-]+)/speaker/(?P<participant_id>[\w\-]+)/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-edit'),
url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/$', views.talk_proposal, name='talk-proposal-edit'), url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/$', views.talk_proposal, name='talk-proposal-edit'),
url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/confirm/$', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm'), url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/confirm/$', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm'),
url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/desist/$', views.talk_acknowledgment, {'confirm': False}, name='talk-desist'), url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/desist/$', views.talk_acknowledgment, {'confirm': False}, name='talk-desist'),
# End backward compatibility
url(r'^volunteer/$', views.volunteer_enrole, name='volunteer-enrole'), url(r'^volunteer/$', views.volunteer_enrole, name='volunteer-enrole'),
url(r'^volunteer/(?P<volunteer_id>[\w\-]+)/$', views.volunteer_home, name='volunteer-home'), url(r'^volunteer/(?P<volunteer_id>[\w\-]+)/$', views.volunteer_home, name='volunteer-home'),
url(r'^volunteer/(?P<volunteer_id>[\w\-]+)/join/(?P<activity>[\w\-]+)/$', views.volunteer_update_activity, {'join': True}, name='volunteer-join'), url(r'^volunteer/(?P<volunteer_id>[\w\-]+)/join/(?P<activity>[\w\-]+)/$', views.volunteer_update_activity, {'join': True}, name='volunteer-join'),
@ -41,7 +56,7 @@ urlpatterns = [
url(r'^staff/schedule/((?P<program_format>[\w]+)/)?$', views.staff_schedule, name='staff-schedule'), url(r'^staff/schedule/((?P<program_format>[\w]+)/)?$', views.staff_schedule, name='staff-schedule'),
url(r'^staff/select2/$', views.Select2View.as_view(), name='django_select2-json'), url(r'^staff/select2/$', views.Select2View.as_view(), name='django_select2-json'),
url(r'^admin/$', views.admin, name='admin'), url(r'^admin/$', views.admin, name='admin'),
url(r'^admin/conference/$', views.conference, name='conference'), url(r'^admin/conference/$', views.conference_edit, name='conference'),
url(r'^schedule/((?P<program_format>[\w]+)/)?$', views.public_schedule, name='public-schedule'), url(r'^schedule/((?P<program_format>[\w]+)/)?$', views.public_schedule, name='public-schedule'),
#url(r'^markdown/$', views.markdown_preview, name='markdown'), #url(r'^markdown/$', views.markdown_preview, name='markdown'),
] ]

View File

@ -20,14 +20,14 @@ from functools import reduce
from mailing.models import Message from mailing.models import Message
from mailing.forms import MessageForm from mailing.forms import MessageForm
from .planning import Program from .planning import Program
from .decorators import staff_required from .decorators import speaker_required, staff_required
from .mixins import StaffRequiredMixin, OnSiteMixin, OnSiteFormMixin from .mixins import StaffRequiredMixin, OnSiteMixin, OnSiteFormMixin
from .utils import is_staff from .utils import is_staff
from .models import Participant, Talk, TalkCategory, Vote, Track, Tag, Room, Volunteer, Activity from .models import Participant, Talk, TalkCategory, Vote, Track, Tag, Room, Volunteer, Activity
from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, \ from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, \
ParticipantForm, ParticipantStaffForm, ParticipantFilterForm, \ ParticipantForm, ParticipantStaffForm, ParticipantFilterForm, \
ConferenceForm, CreateUserForm, TrackForm, RoomForm, \ ConferenceForm, CreateUserForm, TrackForm, RoomForm, \
VolunteerForm, VolunteerFilterForm, \ VolunteerForm, VolunteerFilterForm, MailForm, \
ACCEPTATION_VALUES, CONFIRMATION_VALUES ACCEPTATION_VALUES, CONFIRMATION_VALUES
@ -35,7 +35,7 @@ def home(request):
if request.conference.home: if request.conference.home:
return render(request, 'cfp/home.html') return render(request, 'cfp/home.html')
else: else:
return redirect(reverse('talk-proposal')) return redirect(reverse('proposal-home'))
def volunteer_enrole(request): def volunteer_enrole(request):
@ -139,43 +139,25 @@ def volunteer_details(request, volunteer_id):
}) })
def talk_proposal(request, talk_id=None, participant_id=None): def proposal_home(request):
conference = request.conference
site = conference.site
if is_staff(request, request.user): if is_staff(request, request.user):
categories = TalkCategory.objects.filter(site=site) categories = TalkCategory.objects.filter(site=request.conference.site)
else: else:
categories = conference.opened_categories categories = request.conference.opened_categories
talk = None speaker_form = ParticipantForm(request.POST or None, conference=request.conference, social=False)
participant = None talk_form = TalkForm(request.POST or None, categories=categories)
if request.method == 'POST' and all(map(lambda f: f.is_valid(), [speaker_form, talk_form])):
if talk_id and participant_id: speaker = speaker_form.save(commit=False)
talk = get_object_or_404(Talk, token=talk_id, site=site) speaker.site = request.conference.site
participant = get_object_or_404(Participant, token=participant_id, site=site) speaker.save()
elif not categories.exists():
return render(request, 'cfp/closed.html')
participant_form = ParticipantForm(request.POST or None, instance=participant)
talk_form = TalkForm(request.POST or None, categories=categories, instance=talk)
if request.method == 'POST' and talk_form.is_valid() and participant_form.is_valid():
talk = talk_form.save(commit=False) talk = talk_form.save(commit=False)
talk.site = site talk.site = request.conference.site
participant, created = Participant.objects.get_or_create(email=participant_form.cleaned_data['email'], site=site)
participant_form = ParticipantForm(request.POST, instance=participant)
participant = participant_form.save()
participant.language = request.LANGUAGE_CODE
participant.save()
talk.save() talk.save()
talk.speakers.add(participant) talk.speakers.add(speaker)
base_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain
protocol = 'https' if request.is_secure() else 'http' url_dashboard = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token))
base_url = protocol+'://'+site.domain url_talk_details = base_url + reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk))
url_talk_proposal_edit = base_url + reverse('talk-proposal-edit', args=[talk.token, participant.token]) url_speaker_add = base_url + reverse('proposal-speaker-add', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk))
url_talk_proposal_speaker_add = base_url + reverse('talk-proposal-speaker-add', args=[talk.token])
url_talk_proposal_speaker_edit = base_url + reverse('talk-proposal-speaker-edit', args=[talk.token, participant.token])
body = _("""Hi {}, body = _("""Hi {},
Your talk has been submitted for {}. Your talk has been submitted for {}.
@ -185,9 +167,9 @@ Title: {}
Description: {} Description: {}
You can at anytime: You can at anytime:
- edit your talk: {} - review and edit your profile: {}
- review and edit your talk: {}
- add a new co-speaker: {} - add a new co-speaker: {}
- edit your profile: {}
If you have any question, your can answer to this email. If you have any question, your can answer to this email.
@ -195,89 +177,232 @@ Thanks!
{} {}
""").format(participant.name, conference.name, talk.title, talk.description, url_talk_proposal_edit, url_talk_proposal_speaker_add, url_talk_proposal_speaker_edit, conference.name) """).format(
speaker.name, request.conference.name,talk.title, talk.description,
url_dashboard, url_talk_details, url_speaker_add,
request.conference.name,
)
Message.objects.create( Message.objects.create(
thread=participant.conversation, thread=speaker.conversation,
author=conference, author=request.conference,
from_email=conference.contact_email, from_email=request.conference.contact_email,
content=body, content=body,
) )
messages.success(request, _('You proposition have been successfully submitted!'))
return render(request, 'cfp/complete.html', {'talk': talk, 'participant': participant}) return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
return render(request, 'cfp/proposal_home.html', {
return render(request, 'cfp/propose.html', { 'speaker_form': speaker_form,
'participant_form': participant_form,
'site': site,
'talk_form': talk_form, 'talk_form': talk_form,
}) })
def talk_proposal_speaker_edit(request, talk_id, participant_id=None): def proposal_mail_token(request):
form = MailForm(request.POST or None)
if request.method == 'POST' and form.is_valid():
try:
speaker = Participant.objects.get(site=request.conference.site, email=form.cleaned_data['email'])
except Participant.DoesNotExist:
messages.error(request, _('Sorry, we do not know this email.'))
else:
talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site) base_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain
participant = None dashboard_url = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token))
body = _("""Hi {},
if participant_id: Someone, probably you, ask to access your profile.
participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site) You can edit your talks or add new ones following this url:
participant_form = ParticipantForm(request.POST or None, instance=participant) {}
if request.method == 'POST' and participant_form.is_valid(): If you have any question, your can answer to this email.
participant, created = Participant.objects.get_or_create(email=participant_form.cleaned_data['email'], site=request.conference.site) Sincerely,
participant_form = ParticipantForm(request.POST, instance=participant)
participant = participant_form.save()
participant.save()
talk.speakers.add(participant) {}
return render(request,'cfp/complete.html', {'talk': talk, 'participant': participant}) """).format(speaker.name, dashboard_url, request.conference.name)
Message.objects.create(
return render(request, 'cfp/speaker.html', { thread=speaker.conversation,
'participant_form': participant_form, author=request.conference,
from_email=request.conference.contact_email,
content=body,
)
messages.success(request, _('A email have been sent with a link to access to your profil.'))
return redirect(reverse('proposal-mail-token'))
return render(request, 'cfp/proposal_mail_token.html', {
'form': form,
}) })
def talk_acknowledgment(request, talk_id, confirm, participant_id=None): @speaker_required
# TODO: handle multiple speakers case def proposal_dashboard(request, speaker):
talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site) return render(request, 'cfp/proposal_dashboard.html', {
if participant_id: 'speaker': speaker,
participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site) 'talks': speaker.talk_set.all(),
elif not is_staff(request, request.user): })
raise PermissionDenied
@speaker_required
def proposal_talk_details(request, speaker, talk_id):
talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
return render(request, 'cfp/proposal_talk_details.html', {
'speaker': speaker,
'talk': talk,
})
@speaker_required
def proposal_talk_edit(request, speaker, talk_id=None):
if talk_id:
talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
else: else:
participant = None talk = None
if not talk.accepted: if is_staff(request, request.user):
raise PermissionDenied categories = TalkCategory.objects.filter(site=request.conference.site)
if talk.confirmed != confirm: else:
talk.confirmed = confirm categories = request.conference.opened_categories
form = TalkForm(request.POST or None, categories=categories, instance=talk)
if request.method == 'POST' and form.is_valid():
talk = form.save(commit=False)
talk.site = request.conference.site
talk.save() talk.save()
if confirm: talk.speakers.add(speaker)
confirmation_message= _('Your participation has been taken into account, thank you!') if talk_id:
if participant: messages.success(request, _('Changes saved.'))
thread_note = _('Speaker %(speaker)s confirmed his/her participation.')
else:
thread_note = _('The talk have been confirmed.')
else: else:
confirmation_message = _('We have noted your unavailability.') # TODO: it could be great to receive the proposition by mail
if participant: # but this is not crucial as the speaker already have a link in its mailbox
thread_note = _('Speaker %(speaker)s CANCELLED his/her participation.') messages.success(request, _('You proposition have been successfully submitted!'))
else: return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
thread_note = _('The talk have been cancelled.') return render(request, 'cfp/proposal_talk_form.html', {
if participant_id: 'speaker': speaker,
thread_note = thread_note % {'speaker': participant} 'talk': talk,
Message.objects.create(thread=talk.conversation, author=participant or request.user, content=thread_note) 'form': form,
messages.success(request, confirmation_message) })
else:
@speaker_required
def proposal_talk_acknowledgment(request, speaker, talk_id, confirm):
# TODO: handle multiple speakers case
talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
if not request.conference.disclosed_acceptances or not talk.accepted:
raise PermissionDenied
if talk.confirmed == confirm:
if confirm: if confirm:
messages.warning(request, _('You already confirmed your participation to this talk.')) messages.warning(request, _('You already confirmed your participation to this talk.'))
else: else:
messages.warning(request, _('You already cancelled your participation to this talk.')) 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: else:
return redirect(reverse('talk-details', kwargs=dict(talk_id=talk_id))) talk.confirmed = confirm
talk.save()
if confirm:
confirmation_message= _('Your participation has been taken into account, thank you!')
thread_note = _('Speaker %(speaker)s confirmed his/her participation.' % {'speaker': speaker})
else:
confirmation_message = _('We have noted your unavailability.')
thread_note = _('Speaker %(speaker)s CANCELLED his/her participation.' % {'speaker': speaker})
Message.objects.create(thread=talk.conversation, author=speaker, content=thread_note)
messages.success(request, confirmation_message)
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
# FIXME his this view really useful?
#@speaker_required
#def proposal_speaker_details(request, speaker, talk_id, co_speaker_id):
# talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
# co_speaker = get_object_or_404(Participant, site=request.conference.site, talk_set__pk=talk.pk, pk=co_speaker_id)
# return render(request, 'cfp/proposal_speaker_details.html', {
# 'speaker': speaker,
# 'talk': talk,
# 'co_speaker': co_speaker,
# })
@speaker_required
def proposal_speaker_edit(request, speaker, talk_id=None, co_speaker_id=None):
if talk_id:
talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
if co_speaker_id:
co_speaker = get_object_or_404(Participant, site=request.conference.site, talk__pk=talk.pk, pk=co_speaker_id)
else:
co_speaker = None
else:
talk = None
co_speaker = None
form = ParticipantForm(request.POST or None, conference=request.conference, instance=co_speaker if talk else speaker)
if request.method == 'POST' and form.is_valid():
# TODO: Allow to add a co-speaker which already exists.
# This should be automatically allowed if the speaker already have a talk in common with the co-speaker.
# Otherwise, we should send an speaker request to the other user OR allow the other user to join the talk with his token.
# This last requirements in planned for v3.
edited_speaker = form.save()
if talk:
talk.speakers.add(edited_speaker)
#return redirect(reverse('proposal-speaker-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
else:
return redirect(reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token)))
return render(request, 'cfp/proposal_speaker_form.html', {
'speaker': speaker,
'talk': talk,
'co_speaker': co_speaker,
'form': form,
})
@speaker_required
def proposal_speaker_remove(request, speaker, talk_id, co_speaker_id):
talk = get_object_or_404(Talk, site=request.conference.site, speakers__pk=speaker.pk, pk=talk_id)
co_speaker = get_object_or_404(Participant, site=request.conference.site, talk_set__pk=talk.pk, pk=co_speaker_id)
return redirect(reverse('proposal-speaker-details', kwargs=dict()))
# BACKWARD COMPATIBILITY
def talk_proposal(request, talk_id=None, participant_id=None):
if talk_id and participant_id:
talk = get_object_or_404(Talk, token=talk_id, site=site)
speaker = get_object_or_404(Participant, token=participant_id, site=site)
return render(reverse('proposal-talk-edit', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
else:
return render(reverse('proposal-home'))
# BACKWARD COMPATIBILITY
def talk_proposal_speaker_edit(request, talk_id, participant_id=None):
talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
speaker = talk.speakers.first() # no other choice here…
if participant_id:
co_speaker = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
return redirect(reverse('proposal-speaker-edit', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk, co_speaker_id=co_speaker.pk)))
else:
return redirect(reverse('proposal-speaker-add', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
# TODO: add @staff_required decorator when dropping old links support
def talk_acknowledgment(request, talk_id, confirm, participant_id=None):
talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
if participant_id:
speaker = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
if confirm:
return redirect(reverse('proposal-talk-confirm', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
else:
return redirect(reverse('proposal-talk-desist', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
elif not is_staff(request, request.user):
raise PermissionDenied
if not talk.accepted or talk.confirmed == confirm:
raise PermissionDenied
# TODO: handle multiple speakers case
talk.confirmed = confirm
talk.save()
if confirm:
confirmation_message= _('The speaker confirmation have been noted.')
thread_note = _('The talk have been confirmed.')
else:
confirmation_message = _('The speaker unavailability have been noted.')
thread_note = _('The talk have been cancelled.')
Message.objects.create(thread=talk.conversation, author=request.user, content=thread_note)
messages.success(request, confirmation_message)
return redirect(reverse('talk-details', kwargs=dict(talk_id=talk_id)))
@staff_required @staff_required
@ -529,7 +654,7 @@ class ParticipantUpdate(StaffRequiredMixin, OnSiteMixin, UpdateView):
@staff_required @staff_required
def conference(request): def conference_edit(request):
form = ConferenceForm(request.POST or None, instance=request.conference) form = ConferenceForm(request.POST or None, instance=request.conference)
if request.method == 'POST' and form.is_valid(): if request.method == 'POST' and form.is_valid():

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
<a href="{% url 'home' %}"><span class="glyphicon glyphicon-home"></span>&nbsp;{% trans "Home" %}</a> <a href="{% url 'home' %}"><span class="glyphicon glyphicon-home"></span>&nbsp;{% trans "Home" %}</a>
</li>{% endif %} </li>{% endif %}
<li{% block proposetab %}{% endblock %}> <li{% block proposetab %}{% endblock %}>
<a href="{% url 'talk-proposal' %}"><span class="glyphicon glyphicon-bullhorn"></span>&nbsp;{% trans "Call for participation" %}</a> <a href="{% url 'proposal-home' %}"><span class="glyphicon glyphicon-bullhorn"></span>&nbsp;{% trans "Call for participation" %}</a>
</li> </li>
{% if conference.schedule_available %}<li{% block publicscheduletab %}{% endblock %}> {% if conference.schedule_available %}<li{% block publicscheduletab %}{% endblock %}>
<a href="{% url 'public-schedule' %}"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{% trans "Schedule" %}</a> <a href="{% url 'public-schedule' %}"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{% trans "Schedule" %}</a>