This commit is contained in:
Élie Bouttier 2017-11-04 21:38:38 +01:00
commit 714b85594e
20 changed files with 1196 additions and 414 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,34 @@ 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):
notify = forms.BooleanField(initial=True, required=False, label=_('Notify by mail?'))
def __init__(self, *args, **kwargs):
social = kwargs.pop('social', True)
ask_notify = kwargs.pop('ask_notify', False)
super().__init__(*args, **kwargs)
if not social:
for field in ['twitter', 'linkedin', 'github', 'website', 'facebook', 'mastodon']:
self.fields.pop(field)
if not ask_notify:
self.fields.pop('notify')
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 +260,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 +317,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)
@ -109,16 +115,24 @@ class Participant(PonyConfModel):
objects = ParticipantManager() objects = ParticipantManager()
def get_absolute_url(self): def get_secret_url(self, full=False):
return reverse('participant-details', kwargs={'participant_id': self.token}) url = reverse('proposal-dashboard', kwargs={'speaker_token': self.token})
if full:
url = ('https' if self.site.conference.secure_domain else 'http') + '://' + self.site.domain + url
return url
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):
return str(self.name) return str(self.name)
@property
def co_speaker_set(self):
return Participant.objects.filter(site=self.site, talk__in=self.talk_set.values_list('pk')).exclude(pk=self.pk).order_by('name').distinct()
@property @property
def accepted_talk_set(self): def accepted_talk_set(self):
return self.talk_set.filter(accepted=True) return self.talk_set.filter(accepted=True)

View File

@ -12,4 +12,7 @@
<h2>{% trans "Sorry, the Call for Participation is closed!" %}</h2> <h2>{% trans "Sorry, the Call for Participation is closed!" %}</h2>
{% url 'proposal-mail-token' as mail_token_url %}
<p>{% blocktrans %}If you already have submitted a talk and you want to edit it, please click <a href="{{ mail_token_url }}">here</a>.{% endblocktrans %}</p>
{% endblock %} {% 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,79 @@
{% 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>
{% if conference.opened_categories.exists %}
<p>
<a href="{% url 'proposal-talk-add' speaker_token=speaker.token %}" class="btn btn-primary">{% trans "New proposal" %}</a>
</p>
{% endif %}
{% 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,52 @@
{% 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">
{% if co_speaker_candidates %}
<div class="col-md-8 col-md-offset-2 alert alert-info">
{% trans "You may want to add one of the following speakers:" %}
{% for spkr in co_speaker_candidates %}
{% if forloop.first %}<ul>{% endif %}
<li><a href="{% url 'proposal-speaker-add-existing' speaker_token=speaker.token talk_id=talk.pk speaker_id=spkr.pk %}">{{ spkr }}</a></li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
</div>
{% endif %}
<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,94 @@
{% 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>
{% if conference.opened_categories.exists %}
<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>
{% endif %}
<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!" %})
{% else %}
<a href="{% url 'proposal-speaker-remove' speaker_token=speaker.token talk_id=talk.pk co_speaker_id=spkr.pk %}" class="btn btn-xs btn-danger">
<span class=" glyphicon glyphicon-remove"></span>&nbsp;{% trans "remove" %}
</a>
{% 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

@ -32,6 +32,7 @@
{% if participant.mastodon %}<li><b>{% trans "Mastodon:" %}</b> <a href="{{ participant.mastodon }}">{{ participant.mastodon }}</a></li>{% endif %} {% if participant.mastodon %}<li><b>{% trans "Mastodon:" %}</b> <a href="{{ participant.mastodon }}">{{ participant.mastodon }}</a></li>{% endif %}
{% if participant.phone_number %}<li><b>{% trans "Phone number:" %}</b> {{ participant.phone_number }}</li>{% endif %} {% if participant.phone_number %}<li><b>{% trans "Phone number:" %}</b> {{ participant.phone_number }}</li>{% endif %}
{% if participant.language %}<li><b>{% trans "Language:" %}</b> {{ participant.language }}</li>{% endif %} {% if participant.language %}<li><b>{% trans "Language:" %}</b> {{ participant.language }}</li>{% endif %}
<li><b>{% trans "Secret link:" %}</b> <a href="{{ participant.get_secret_url }}">{{ participant.token }}</a></li>
</ul> </ul>
<h2>{% trans "Talks" %}</h2> <h2>{% trans "Talks" %}</h2>

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,28 @@ 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>[0-9]+)/$', views.proposal_talk_details, name='proposal-talk-details'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/edit/$', views.proposal_talk_edit, name='proposal-talk-edit'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/speaker/add/$', views.proposal_speaker_edit, name='proposal-speaker-add'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/speaker/add/(?P<speaker_id>[0-9]+)/$', views.proposal_speaker_add, name='proposal-speaker-add-existing'),
#url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/speaker/(?P<co_speaker_id>[0-9]+)/$', views.proposal_speaker_details, name='proposal-speaker-details'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/speaker/(?P<co_speaker_id>[0-9]+)/edit/$', views.proposal_speaker_edit, name='proposal-speaker-edit'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/speaker/(?P<co_speaker_id>[0-9]+)/remove/$', views.proposal_speaker_remove, name='proposal-speaker-remove'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/confirm/$', views.proposal_talk_acknowledgment, {'confirm': True}, name='proposal-talk-confirm'),
url(r'^cfp/(?P<speaker_token>[\w\-]+)/talk/(?P<talk_id>[0-9]+)/desist/$', views.proposal_talk_acknowledgment, {'confirm': False}, name='proposal-talk-desist'),
# 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 +57,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):
@ -46,7 +46,6 @@ def volunteer_enrole(request):
volunteer = form.save(commit=False) volunteer = form.save(commit=False)
volunteer.language = request.LANGUAGE_CODE volunteer.language = request.LANGUAGE_CODE
volunteer.save() volunteer.save()
volunteer_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain + volunteer.get_absolute_url()
body = _("""Hi {}, body = _("""Hi {},
Thank your for your help in the organization of the conference {}! Thank your for your help in the organization of the conference {}!
@ -59,7 +58,7 @@ Thanks!
{} {}
""").format(volunteer.name, request.conference.name, volunteer_url, request.conference.name) """).format(volunteer.name, request.conference.name, volunteer.get_secret_url(full=True), request.conference.name)
#Message.objects.create( #Message.objects.create(
# thread=volunteer.conversation, # thread=volunteer.conversation,
# author=request.conference, # author=request.conference,
@ -139,43 +138,27 @@ 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 if not categories.exists():
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') return render(request, 'cfp/closed.html')
speaker_form = ParticipantForm(request.POST or None, conference=request.conference, social=False)
participant_form = ParticipantForm(request.POST or None, instance=participant) talk_form = TalkForm(request.POST or None, categories=categories)
talk_form = TalkForm(request.POST or None, categories=categories, instance=talk) if request.method == 'POST' and all(map(lambda f: f.is_valid(), [speaker_form, talk_form])):
speaker = speaker_form.save(commit=False)
if request.method == 'POST' and talk_form.is_valid() and participant_form.is_valid(): speaker.site = request.conference.site
speaker.save()
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 +168,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 +178,283 @@ 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):
talk, co_speaker, co_speaker_candidates = None, None, 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_candidates = speaker.co_speaker_set.exclude(pk__in=talk.speakers.values_list('pk'))
form = ParticipantForm(request.POST or None, conference=request.conference,
instance=co_speaker if talk else speaker, ask_notify=talk and not co_speaker)
if request.method == 'POST' and form.is_valid():
edited_speaker = form.save()
if talk:
talk.speakers.add(edited_speaker)
if co_speaker_id:
messages.success(request, _('Changes saved.'))
else:
if form.cleaned_data['notify']:
base_url = ('https' if request.is_secure() else 'http') + '://' + request.conference.site.domain
url_dashboard = base_url + reverse('proposal-dashboard', kwargs=dict(speaker_token=edited_speaker.token))
url_talk_details = base_url + reverse('proposal-talk-details', kwargs=dict(speaker_token=edited_speaker.token, talk_id=talk.pk))
url_speaker_add = base_url + reverse('proposal-speaker-add', kwargs=dict(speaker_token=edited_speaker.token, talk_id=talk.pk))
body = _("""Hi {},
{} add you as a co-speaker for the conference {}.
Here is a summary of the talk:
Title: {}
Description: {}
You can at anytime:
- review and edit your profile: {}
- review and edit the talk: {}
- add another co-speaker: {}
If you have any question, your can answer to this email.
Thanks!
{}
""").format(
edited_speaker.name, speaker.name, request.conference.name,
talk.title, talk.description,
url_dashboard, url_talk_details, url_speaker_add,
request.conference.name,
)
Message.objects.create(
thread=edited_speaker.conversation,
author=request.conference,
from_email=request.conference.contact_email,
content=body,
)
messages.success(request, _('Co-speaker successfully added to the talk.'))
#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,
'co_speaker_candidates': co_speaker_candidates,
'form': form,
})
@speaker_required
def proposal_speaker_add(request, speaker, talk_id, 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, pk__in=speaker.co_speaker_set.values_list('pk'))
talk.speakers.add(co_speaker)
messages.success(request, _('Co-speaker successfully added to the talk.'))
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk_id)))
# TODO: ask for confirmation (with POST request needed)
@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__pk=talk.pk, pk=co_speaker_id)
# prevent speaker from removing his/her self
if co_speaker.pk == speaker.pk:
raise PermissionDenied
talk.speakers.remove(co_speaker)
messages.success(request, _('Co-speaker successfully removed from the talk.'))
return redirect(reverse('proposal-talk-details', kwargs=dict(speaker_token=speaker.token, talk_id=talk_id)))
# 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=request.conference.site)
speaker = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
return redirect(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)
if participant_id:
speaker = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
return redirect(reverse('proposal-profile-edit', kwargs=dict(speaker_token=speaker.token)))
else:
speaker = talk.speakers.first() # no other choice here…
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 +706,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>