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.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):

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):
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

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'))
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):

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 %}
<div class="page-header">
<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>
</div>
@ -16,8 +23,7 @@
<div class="col-md-12">
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
{% csrf_token %}
{{ participant_form|crispy }}
{{ talk_form|crispy }}
{{ 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>

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)
minutes = value%60
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 = [
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/(?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\-]+)/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'),
# End backward compatibility
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\-]+)/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/select2/$', views.Select2View.as_view(), name='django_select2-json'),
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'^markdown/$', views.markdown_preview, name='markdown'),
]

View File

@ -20,14 +20,14 @@ from functools import reduce
from mailing.models import Message
from mailing.forms import MessageForm
from .planning import Program
from .decorators import staff_required
from .decorators import speaker_required, staff_required
from .mixins import StaffRequiredMixin, OnSiteMixin, OnSiteFormMixin
from .utils import is_staff
from .models import Participant, Talk, TalkCategory, Vote, Track, Tag, Room, Volunteer, Activity
from .forms import TalkForm, TalkStaffForm, TalkFilterForm, TalkActionForm, \
ParticipantForm, ParticipantStaffForm, ParticipantFilterForm, \
ConferenceForm, CreateUserForm, TrackForm, RoomForm, \
VolunteerForm, VolunteerFilterForm, \
VolunteerForm, VolunteerFilterForm, MailForm, \
ACCEPTATION_VALUES, CONFIRMATION_VALUES
@ -35,7 +35,7 @@ def home(request):
if request.conference.home:
return render(request, 'cfp/home.html')
else:
return redirect(reverse('talk-proposal'))
return redirect(reverse('proposal-home'))
def volunteer_enrole(request):
@ -139,43 +139,25 @@ def volunteer_details(request, volunteer_id):
})
def talk_proposal(request, talk_id=None, participant_id=None):
conference = request.conference
site = conference.site
def proposal_home(request):
if is_staff(request, request.user):
categories = TalkCategory.objects.filter(site=site)
categories = TalkCategory.objects.filter(site=request.conference.site)
else:
categories = conference.opened_categories
talk = None
participant = None
if talk_id and participant_id:
talk = get_object_or_404(Talk, token=talk_id, site=site)
participant = get_object_or_404(Participant, token=participant_id, site=site)
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():
categories = request.conference.opened_categories
speaker_form = ParticipantForm(request.POST or None, conference=request.conference, social=False)
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])):
speaker = speaker_form.save(commit=False)
speaker.site = request.conference.site
speaker.save()
talk = talk_form.save(commit=False)
talk.site = 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.site = request.conference.site
talk.save()
talk.speakers.add(participant)
protocol = 'https' if request.is_secure() else 'http'
base_url = protocol+'://'+site.domain
url_talk_proposal_edit = base_url + reverse('talk-proposal-edit', args=[talk.token, participant.token])
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])
talk.speakers.add(speaker)
base_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain
url_dashboard = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token))
url_talk_details = base_url + reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk))
url_speaker_add = base_url + reverse('proposal-speaker-add', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk))
body = _("""Hi {},
Your talk has been submitted for {}.
@ -185,9 +167,9 @@ Title: {}
Description: {}
You can at anytime:
- edit your talk: {}
- review and edit your profile: {}
- review and edit your talk: {}
- add a new co-speaker: {}
- edit your profile: {}
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(
thread=participant.conversation,
author=conference,
from_email=conference.contact_email,
thread=speaker.conversation,
author=request.conference,
from_email=request.conference.contact_email,
content=body,
)
return render(request, 'cfp/complete.html', {'talk': talk, 'participant': participant})
return render(request, 'cfp/propose.html', {
'participant_form': participant_form,
'site': site,
messages.success(request, _('You proposition have been successfully submitted!'))
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
return render(request, 'cfp/proposal_home.html', {
'speaker_form': speaker_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)
participant = None
base_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain
dashboard_url = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=speaker.token))
body = _("""Hi {},
if participant_id:
participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
Someone, probably you, ask to access your profile.
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)
participant_form = ParticipantForm(request.POST, instance=participant)
participant = participant_form.save()
participant.save()
Sincerely,
talk.speakers.add(participant)
{}
return render(request,'cfp/complete.html', {'talk': talk, 'participant': participant})
return render(request, 'cfp/speaker.html', {
'participant_form': participant_form,
""").format(speaker.name, dashboard_url, request.conference.name)
Message.objects.create(
thread=speaker.conversation,
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):
# 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
@speaker_required
def proposal_dashboard(request, speaker):
return render(request, 'cfp/proposal_dashboard.html', {
'speaker': speaker,
'talks': speaker.talk_set.all(),
})
@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:
participant = None
if not talk.accepted:
raise PermissionDenied
if talk.confirmed != confirm:
talk.confirmed = confirm
talk = None
if is_staff(request, request.user):
categories = TalkCategory.objects.filter(site=request.conference.site)
else:
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()
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.')
talk.speakers.add(speaker)
if talk_id:
messages.success(request, _('Changes saved.'))
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:
# TODO: it could be great to receive the proposition by mail
# but this is not crucial as the speaker already have a link in its mailbox
messages.success(request, _('You proposition have been successfully submitted!'))
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk.pk)))
return render(request, 'cfp/proposal_talk_form.html', {
'speaker': speaker,
'talk': talk,
'form': form,
})
@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:
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)))
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
@ -529,7 +654,7 @@ class ParticipantUpdate(StaffRequiredMixin, OnSiteMixin, UpdateView):
@staff_required
def conference(request):
def conference_edit(request):
form = ConferenceForm(request.POST or None, instance=request.conference)
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>
</li>{% endif %}
<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>
{% if conference.schedule_available %}<li{% block publicscheduletab %}{% endblock %}>
<a href="{% url 'public-schedule' %}"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{% trans "Schedule" %}</a>