diff --git a/cfp/decorators.py b/cfp/decorators.py index a5ca0ac..40964bc 100644 --- a/cfp/decorators.py +++ b/cfp/decorators.py @@ -1,9 +1,22 @@ -from functools import wraps - from django.core.exceptions import PermissionDenied from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404 + +from functools import wraps from cfp.utils import is_staff +from cfp.models import Participant + + +def speaker_required(view_func): + def wrapped_view(request, **kwargs): + speaker_token = kwargs.pop('speaker_token') + # TODO v3: if no speaker token is provided, we should check for a logged user, and if so, + # we should check if his/her participating at current conference + speaker = get_object_or_404(Participant, site=request.conference.site, token=speaker_token) + kwargs['speaker'] = speaker + return view_func(request, **kwargs) + return wraps(view_func)(wrapped_view) def staff_required(view_func): diff --git a/cfp/forms.py b/cfp/forms.py index b0bef9e..a2bc667 100644 --- a/cfp/forms.py +++ b/cfp/forms.py @@ -36,6 +36,27 @@ CONFIRMATION_VALUES = [ ] +class OnSiteNamedModelForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.conference = kwargs.pop('conference') + super().__init__(*args, **kwargs) + + # we should manually check (site, name) uniqueness as the site is not part of the form + def clean_name(self): + name = self.cleaned_data['name'] + if (not self.instance or self.instance.name != name) \ + and self._meta.model.objects.filter(site=self.conference.site, name=name).exists(): + raise self.instance.unique_error_message(self._meta.model, ['name']) + return name + + def save(self, commit=True): + obj = super().save(commit=False) + obj.site = self.conference.site + if commit: + obj.save() + return obj + + class VolunteerFilterForm(forms.Form): activity = forms.MultipleChoiceField( label=_('Activity'), @@ -174,15 +195,29 @@ class TalkActionForm(forms.Form): self.fields['room'].choices = [(None, "---------")] + list(rooms.values_list('slug', 'name')) -ParticipantForm = modelform_factory(Participant, fields=('name', 'email', 'biography')) +class ParticipantForm(OnSiteNamedModelForm): + def __init__(self, *args, **kwargs): + social = kwargs.pop('social', True) + super().__init__(*args, **kwargs) + if not social: + for field in ['twitter', 'linkedin', 'github', 'website', 'facebook', 'mastodon']: + self.fields.pop(field) + + class Meta: + model = Participant + fields = ['name', 'email', 'biography', 'twitter', 'linkedin', 'github', 'website', 'facebook', 'mastodon'] + + def clean_email(self): + email = self.cleaned_data['email'] + if (not self.instance or self.instance.email != email) \ + and self._meta.model.objects.filter(site=self.conference.site, email=email).exists(): + raise self.instance.unique_error_message(self._meta.model, ['email']) + return email class ParticipantStaffForm(ParticipantForm): class Meta(ParticipantForm.Meta): - fields = ('name', 'vip', 'email', 'biography') - labels = { - 'name': _('Name'), - } + fields = ['name', 'vip', 'email', 'phone_number', 'notes'] + ParticipantForm.Meta.fields[3:] class ParticipantFilterForm(forms.Form): @@ -220,6 +255,10 @@ class ParticipantFilterForm(forms.Form): self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name')) +class MailForm(forms.Form): + email = forms.EmailField(required=True, label=_('Email')) + + class UsersWidget(ModelSelect2MultipleWidget): model = User search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ] @@ -273,27 +312,6 @@ class CreateUserForm(forms.ModelForm): return user -class OnSiteNamedModelForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - self.conference = kwargs.pop('conference') - super().__init__(*args, **kwargs) - - # we should manually check (site, name) uniqueness as the site is not part of the form - def clean_name(self): - name = self.cleaned_data['name'] - if (not self.instance or self.instance.name != name) \ - and self._meta.model.objects.filter(site=self.conference.site, name=name).exists(): - raise self.instance.unique_error_message(self._meta.model, ['name']) - return name - - def save(self, commit=True): - obj = super().save(commit=False) - obj.site = self.conference.site - if commit: - obj.save() - return obj - - class TrackForm(OnSiteNamedModelForm): class Meta: model = Track diff --git a/cfp/migrations/0018_auto_20171104_1227.py b/cfp/migrations/0018_auto_20171104_1227.py new file mode 100644 index 0000000..7e9884b --- /dev/null +++ b/cfp/migrations/0018_auto_20171104_1227.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.5 on 2017-11-04 12:27 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cfp', '0017_auto_20171103_1922'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='acceptances_disclosure_date', + field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Acceptances disclosure date'), + ), + migrations.AlterField( + model_name='participant', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + ] diff --git a/cfp/models.py b/cfp/models.py index 09090eb..f9c8ff8 100644 --- a/cfp/models.py +++ b/cfp/models.py @@ -34,6 +34,7 @@ class Conference(models.Model): reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email')) staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members')) secure_domain = models.BooleanField(default=True, verbose_name=_('Secure domain (HTTPS)')) + acceptances_disclosure_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Acceptances disclosure date')) schedule_publishing_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Schedule publishing date')) schedule_redirection_url = models.URLField(blank=True, default='', verbose_name=_('Schedule redirection URL'), help_text=_('If specified, schedule tab will redirect to this URL.')) @@ -56,6 +57,11 @@ class Conference(models.Model): .filter(Q(opening_date__isnull=True) | Q(opening_date__lte=now))\ .filter(Q(closing_date__isnull=True) | Q(closing_date__gte=now)) + @property + def disclosed_acceptances(self): + # acceptances are automatically disclosed if the schedule is published + return self.acceptances_disclosure_date and self.acceptances_disclosure_date <= timezone.now() or self.schedule_available + @property def schedule_available(self): return self.schedule_publishing_date and self.schedule_publishing_date <= timezone.now() @@ -90,7 +96,7 @@ class ParticipantManager(models.Manager): class Participant(PonyConfModel): site = models.ForeignKey(Site, on_delete=models.CASCADE) - name = models.CharField(max_length=128, verbose_name=_('Your Name')) + name = models.CharField(max_length=128, verbose_name=_('Name')) email = models.EmailField() biography = models.TextField(verbose_name=_('Biography')) token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) @@ -110,10 +116,11 @@ class Participant(PonyConfModel): objects = ParticipantManager() def get_absolute_url(self): - return reverse('participant-details', kwargs={'participant_id': self.token}) + return reverse('proposal-dashboard', kwargs={'speaker_token': self.token}) class Meta: # A User can participe only once to a Conference (= Site) + unique_together = ('site', 'name') unique_together = ('site', 'email') def __str__(self): diff --git a/cfp/templates/cfp/closed.html b/cfp/templates/cfp/closed.html deleted file mode 100644 index 792703c..0000000 --- a/cfp/templates/cfp/closed.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} - -{% block proposetab %} class="active"{% endblock %} - -{% block content %} -
{% trans "Thanks for your proposal" %} {{ participant }} !
-{% trans "You can at anytime:" %} -
{% trans "An email has been sent to you with those URLs" %}
-+
+ {% if speaker.biography %} + {{ speaker.biography|linebreaksbr }} + {% else %} + {% trans "No biography." %} + {% endif %} +
+ ++ {% for talk in talks %} + {% if forloop.first %} +
+ {% trans "New proposal" %} +
+{% endblock %} diff --git a/cfp/templates/cfp/proposal_home.html b/cfp/templates/cfp/proposal_home.html new file mode 100644 index 0000000..6622729 --- /dev/null +++ b/cfp/templates/cfp/proposal_home.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% load ponyconf_tags i18n %} + +{% block proposetab %} class="active"{% endblock %} + +{% block content %} + +{% trans "Reviewing in progress, we will keep you informed by mail." %}
+{% elif talk.accepted %} +{% trans "Accepted!" %}
+{% if talk.confirmed is None %} ++ {% trans "Please confirm your participation:" %} + {% trans "I will be there!" %} + {% trans "Sorry, couldn't make it :-(" %} +
+{% elif talk.confirmed %} ++ {% trans "Sorry, I have to cancel." %} +
+{% else %} ++ {% trans "Good news, I finally could be there!" %} +
+{% endif %} +{% else %} +{% trans "Sorry, refused :-(" %}
+{% endif %} + ++ {% for spkr in talk.speakers.all %} + {% if forloop.first %}
+ {% if talk.description %} + {{ talk.description|linebreaksbr }} + {% else %} + {% trans "No description provided." %} + {% endif %} +
+ ++ {% if talk.notes %} + {{ talk.notes|linebreaksbr }} + {% else %} + {% trans "No description provided." %} + {% endif %} +
+{% endblock %} diff --git a/cfp/templates/cfp/propose.html b/cfp/templates/cfp/proposal_talk_form.html similarity index 61% rename from cfp/templates/cfp/propose.html rename to cfp/templates/cfp/proposal_talk_form.html index a59db31..b7ba544 100644 --- a/cfp/templates/cfp/propose.html +++ b/cfp/templates/cfp/proposal_talk_form.html @@ -8,7 +8,14 @@ {% block content %}