diff --git a/accounts/__init__.py b/accounts/__init__.py
deleted file mode 100644
index 8319823..0000000
--- a/accounts/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-default_app_config = 'accounts.apps.AccountsConfig'
diff --git a/accounts/admin.py b/accounts/admin.py
deleted file mode 100644
index 11bd7e9..0000000
--- a/accounts/admin.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from django.contrib import admin
-
-from accounts.models import Participation, Profile, Transport, Connector
-
-
-admin.site.register(Profile) # FIXME extend user admin
-admin.site.register(Participation)
-admin.site.register(Transport)
-admin.site.register(Connector)
diff --git a/accounts/apps.py b/accounts/apps.py
deleted file mode 100644
index cf90676..0000000
--- a/accounts/apps.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.apps import AppConfig
-from django.db.models.signals import post_migrate
-
-
-class AccountsConfig(AppConfig):
- name = 'accounts'
-
- def ready(self):
- import accounts.signals # noqa
- post_migrate.connect(accounts.signals.create_default_options, sender=self)
diff --git a/accounts/forms.py b/accounts/forms.py
deleted file mode 100644
index 8ab1146..0000000
--- a/accounts/forms.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from django import forms
-from django.contrib.auth.models import User
-from django.forms.models import modelform_factory
-from django.utils.translation import ugettext_lazy as _
-
-from django_select2.forms import Select2Widget
-
-from .models import Participation, Profile
-
-UserForm = modelform_factory(User, fields=['first_name', 'last_name', 'email', 'username'])
-
-ProfileForm = modelform_factory(Profile, fields=['phone_number', 'biography', 'twitter', 'website', 'linkedin', 'facebook', 'mastodon'])
-
-ParticipationForm = modelform_factory(Participation,
- fields=['need_transport', 'transport', 'transport_city_outward', 'transport_city_return',
- 'accommodation',
- 'connector', 'sound', 'videotaped',
- 'video_licence', 'constraints'],
- widgets={'transport': forms.CheckboxSelectMultiple(),
- 'connector': forms.CheckboxSelectMultiple()},
- help_texts = {
- 'constraints': _('For example, you need to be back on saturday evening, you cannot eat meat.'),
- })
-
-ProfileOrgaForm = modelform_factory(Profile, fields=['biography'])
-
-ParticipationOrgaForm = modelform_factory(Participation,
- fields=['need_transport', 'transport', 'transport_city_outward', 'transport_city_return', 'transport_booked',
- 'accommodation', 'accommodation_booked',
- 'connector', 'sound', 'videotaped',
- 'video_licence',
- 'constraints', 'notes', 'orga'],
- widgets={'transport': forms.CheckboxSelectMultiple(),
- 'connector': forms.CheckboxSelectMultiple()})
-
-
-class ParticipationField(forms.ModelChoiceField):
- def label_from_instance(self, obj):
- return obj.profile.__str__()
-
-
-class NewParticipationForm(forms.Form):
- def __init__(self, *args, **kwargs):
- site = kwargs.pop('site')
- super().__init__(*args, **kwargs)
- queryset = User.objects.exclude(participation__site=site).all()
- self.fields['participant'] = ParticipationField(queryset, widget=Select2Widget(),
- label='Add participant from existing account')
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
deleted file mode 100644
index 36a5856..0000000
--- a/accounts/migrations/0001_initial.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-01-13 10:49
-from __future__ import unicode_literals
-
-import accounts.utils
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('sites', '0002_alter_domain_unique'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='AvailabilityTimeslot',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('start', models.DateTimeField(blank=True)),
- ('end', models.DateTimeField(blank=True)),
- ],
- ),
- migrations.CreateModel(
- name='Connector',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=64, unique=True)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.CreateModel(
- name='Participation',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('need_transport', models.NullBooleanField(default=None, verbose_name='Defray transportation?')),
- ('arrival', models.DateTimeField(blank=True, null=True)),
- ('departure', models.DateTimeField(blank=True, null=True)),
- ('transport_city_outward', models.CharField(blank=True, default='', max_length=256, verbose_name='Departure city')),
- ('transport_city_return', models.CharField(blank=True, default='', help_text='If different from departure city', max_length=256, verbose_name='Return city')),
- ('transport_booked', models.BooleanField(default=False)),
- ('accommodation', models.IntegerField(blank=True, choices=[(0, 'No'), (1, 'Hotel'), (2, 'Homestay')], null=True, verbose_name='Need accommodation?')),
- ('accommodation_booked', models.BooleanField(default=False)),
- ('constraints', models.TextField(blank=True, verbose_name='Constraints')),
- ('sound', models.BooleanField(default=False, verbose_name='I need sound')),
- ('videotaped', models.BooleanField(default=True, verbose_name="I'm ok to be recorded on video")),
- ('video_licence', models.IntegerField(choices=[(1, 'CC-Zero'), (2, 'CC-BY'), (3, 'CC-BY-SA'), (4, 'CC-BY-ND'), (5, 'CC-BY-NC'), (6, 'CC-BY-NC-SA'), (7, 'CC-BY-NC-ND')], default=2, verbose_name='Video licence')),
- ('notes', models.TextField(blank=True, default='', help_text='This field is only visible by organizers.', verbose_name='Notes')),
- ('orga', models.BooleanField(default=False)),
- ('connector', models.ManyToManyField(blank=True, to='accounts.Connector', verbose_name='I can output')),
- ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
- ],
- ),
- migrations.CreateModel(
- name='Profile',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('phone_number', models.CharField(blank=True, default='', max_length=16, verbose_name='Phone number')),
- ('biography', models.TextField(blank=True, verbose_name='Biography')),
- ('email_token', models.CharField(default=accounts.utils.generate_user_uid, max_length=12, unique=True)),
- ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.CreateModel(
- name='Transport',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('name', models.CharField(max_length=64, unique=True)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.AddField(
- model_name='participation',
- name='transport',
- field=models.ManyToManyField(blank=True, to='accounts.Transport', verbose_name='I want to travel by'),
- ),
- migrations.AddField(
- model_name='participation',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
- ),
- migrations.AddField(
- model_name='availabilitytimeslot',
- name='participation',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='availabilities', to='accounts.Participation'),
- ),
- migrations.AlterUniqueTogether(
- name='participation',
- unique_together=set([('site', 'user')]),
- ),
- ]
diff --git a/accounts/migrations/0002_auto_20170429_2134.py b/accounts/migrations/0002_auto_20170429_2134.py
deleted file mode 100644
index daa957c..0000000
--- a/accounts/migrations/0002_auto_20170429_2134.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-04-29 21:34
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('accounts', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='profile',
- name='facebook',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='Facebook'),
- ),
- migrations.AddField(
- model_name='profile',
- name='linkedin',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='LinkedIn'),
- ),
- migrations.AddField(
- model_name='profile',
- name='mastodon',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='Mastodon'),
- ),
- migrations.AddField(
- model_name='profile',
- name='twitter',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='Twitter'),
- ),
- migrations.AddField(
- model_name='profile',
- name='website',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='Website'),
- ),
- ]
diff --git a/accounts/migrations/0003_profile_github.py b/accounts/migrations/0003_profile_github.py
deleted file mode 100644
index cb5cb0f..0000000
--- a/accounts/migrations/0003_profile_github.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11 on 2017-04-30 11:29
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('accounts', '0002_auto_20170429_2134'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='profile',
- name='github',
- field=models.CharField(blank=True, default='', max_length=100, verbose_name='Github'),
- ),
- ]
diff --git a/accounts/mixins.py b/accounts/mixins.py
deleted file mode 100644
index d1a5689..0000000
--- a/accounts/mixins.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.contrib.auth.mixins import UserPassesTestMixin
-
-from .utils import is_orga, is_staff
-
-
-class OrgaRequiredMixin(UserPassesTestMixin):
- def test_func(self):
- return is_orga(self.request, self.request.user)
-
-
-class StaffRequiredMixin(UserPassesTestMixin):
- def test_func(self):
- return is_staff(self.request, self.request.user)
-
-
-class SuperuserRequiredMixin(UserPassesTestMixin):
- def test_func(self):
- return self.request.user.is_superuser
diff --git a/accounts/models.py b/accounts/models.py
deleted file mode 100644
index 79b5f1c..0000000
--- a/accounts/models.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from enum import IntEnum
-
-from django.contrib.auth.models import User
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-from django.utils.translation import ugettext
-
-from ponyconf.utils import PonyConfModel, enum_to_choices
-
-from .utils import generate_user_uid
-
-
-class Profile(PonyConfModel):
-
- user = models.OneToOneField(User)
- phone_number = models.CharField(max_length=16, blank=True, default='', verbose_name=_('Phone number'))
- biography = models.TextField(blank=True, verbose_name=_('Biography'))
- email_token = models.CharField(max_length=12, default=generate_user_uid, unique=True)
-
- twitter = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Twitter'))
- linkedin = models.CharField(max_length=100, blank=True, default='', verbose_name=_('LinkedIn'))
- github = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Github'))
- website = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Website'))
- facebook = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Facebook'))
- mastodon = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Mastodon'))
-
- def __str__(self):
- return self.user.get_full_name() or self.user.username
-
- def get_absolute_url(self):
- return reverse('profile')
-
-
-class Option(models.Model):
- name = models.CharField(max_length=64, unique=True)
-
- class Meta:
- abstract = True
-
- def __str__(self):
- return ugettext(self.name)
-
-
-class Transport(Option):
- pass
-
-
-class Connector(Option):
- pass
-
-
-class Participation(PonyConfModel):
-
- LICENCES = IntEnum('Video licence', 'CC-Zero CC-BY CC-BY-SA CC-BY-ND CC-BY-NC CC-BY-NC-SA CC-BY-NC-ND')
- ACCOMMODATION_NO = 0
- ACCOMMODATION_HOTEL = 1
- ACCOMMODATION_HOMESTAY = 2
- ACCOMMODATION_CHOICES = (
- (ACCOMMODATION_NO, _('No')),
- (ACCOMMODATION_HOTEL, _('Hotel')),
- (ACCOMMODATION_HOMESTAY, _('Homestay')),
- )
-
- site = models.ForeignKey(Site, on_delete=models.CASCADE)
- user = models.ForeignKey(User)
-
-
- need_transport = models.NullBooleanField(verbose_name=_('Defray transportation?'), default=None)
- arrival = models.DateTimeField(blank=True, null=True)
- departure = models.DateTimeField(blank=True, null=True)
- transport = models.ManyToManyField(Transport, verbose_name=_("I want to travel by"), blank=True)
- transport_city_outward = models.CharField(blank=True, default='', max_length=256, verbose_name=_("Departure city"))
- transport_city_return = models.CharField(blank=True, default='', max_length=256, verbose_name=_("Return city"), help_text=_("If different from departure city"))
- transport_booked = models.BooleanField(default=False)
-
- accommodation = models.IntegerField(choices=ACCOMMODATION_CHOICES, verbose_name=_('Need accommodation?'), null=True, blank=True)
- accommodation_booked = models.BooleanField(default=False)
-
- constraints = models.TextField(blank=True, verbose_name=_("Constraints"))
- connector = models.ManyToManyField(Connector, verbose_name=_("I can output"), blank=True)
- sound = models.BooleanField(_("I need sound"), default=False)
-
- videotaped = models.BooleanField(_("I'm ok to be recorded on video"), default=True)
- video_licence = models.IntegerField(choices=enum_to_choices(LICENCES), default=2, verbose_name=_("Video licence"))
-
- notes = models.TextField(default='', blank=True, verbose_name=_("Notes"), help_text=_('This field is only visible by organizers.'))
- orga = models.BooleanField(default=False)
-
- class Meta:
- # A User can participe only once to a Conference (= Site)
- unique_together = ('site', 'user')
-
- def __str__(self):
- return str(self.user.profile)
-
- def get_absolute_url(self):
- return reverse('show-participant', kwargs={'username': self.user.username})
-
- def is_orga(self):
- return self.orga
-
- def is_staff(self):
- return self.is_orga() or self.topic_set.exists() or self.track_set.exists()
-
- @property
- def topic_set(self):
- return self.user.topic_set.filter(site=self.site)
-
- @property
- def track_set(self):
- return self.user.track_set.filter(site=self.site)
-
- @property
- def talk_set(self):
- return self.user.talk_set.filter(site=self.site)
-
- @property
- def accepted_talk_set(self):
- return self.talk_set.filter(accepted=True)
- @property
- def pending_talk_set(self):
- return self.talk_set.filter(accepted=None)
- @property
- def refused_talk_set(self):
- return self.talk_set.filter(accepted=False)
- @property
- def not_refused_talk_set(self): # accepted + pending
- return self.talk_set.exclude(accepted=False)
-
- # return True, False or None if availabilities have not been filled
- def is_available(self, start, end=None):
- if not self.availabilities.exists():
- return None
- for timeslot in self.availabilities.all():
- if start < timeslot.start:
- continue
- if start > timeslot.end:
- continue
- if end:
- assert(start < end)
- if end > timeslot.end:
- continue
- return True
- return False
-
-
-class AvailabilityTimeslot(models.Model):
-
- participation = models.ForeignKey(Participation, related_name='availabilities')
- start = models.DateTimeField(blank=True)
- end = models.DateTimeField(blank=True)
diff --git a/accounts/signals.py b/accounts/signals.py
deleted file mode 100644
index 85726f9..0000000
--- a/accounts/signals.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.models import User
-from django.contrib.auth.signals import user_logged_in, user_logged_out
-from django.contrib.sites.shortcuts import get_current_site
-from django.db.models.signals import post_save
-from django.dispatch import receiver
-from django.utils.translation import ugettext_lazy as _
-from django.utils.translation import ugettext_noop
-
-from ponyconf.decorators import disable_for_loaddata
-
-from .models import Connector, Participation, Profile, Transport
-
-
-def create_default_options(sender, **kwargs):
- Transport.objects.get_or_create(name=ugettext_noop('Train'))
- Transport.objects.get_or_create(name=ugettext_noop('Plane'))
- Transport.objects.get_or_create(name=ugettext_noop('Carpooling'))
- Connector.objects.get_or_create(name=ugettext_noop('VGA'))
- Connector.objects.get_or_create(name=ugettext_noop('HDMI'))
- Connector.objects.get_or_create(name=ugettext_noop('miniDP'))
- Connector.objects.get_or_create(name=ugettext_noop('I need a computer'))
-
-
-@receiver(user_logged_in)
-def on_user_logged_in(sender, request, user, **kwargs):
- participation, created = Participation.objects.get_or_create(user=user, site=get_current_site(request))
- if user.is_superuser:
- participation.orga = True
- participation.save()
- if created:
- messages.info(request, "Please check your profile!\n", fail_silently=True) # FIXME
- messages.success(request, _('Welcome!'), fail_silently=True) # FIXME
-
-
-@receiver(user_logged_out)
-def on_user_logged_out(sender, request, **kwargs):
- messages.success(request, _('Goodbye!'), fail_silently=True) # FIXME
-
-
-@receiver(post_save, sender=User, weak=False, dispatch_uid='create_profile')
-@disable_for_loaddata
-def create_profile(sender, instance, created, **kwargs):
- if created:
- Profile.objects.create(user=instance)
diff --git a/accounts/templates/accounts/participant_details.html b/accounts/templates/accounts/participant_details.html
deleted file mode 100644
index 581fc52..0000000
--- a/accounts/templates/accounts/participant_details.html
+++ /dev/null
@@ -1,76 +0,0 @@
-{% extends 'staff.html' %}
-
-{% load accounts_tags i18n avatar_tags %}
-
-{% block participantstab %} class="active"{% endblock %}
-
-{% block content %}
-
-
{{ profile }}
-
-{% if request|staff %}
-{% trans "Contact" %}
-{% endif %}
-{% if request|edit_profile:profile %}
-{% trans "Edit" %}
-{% endif %}
-
-
-
-{% trans "Biography" %}
-{{ profile.biography }}
-
-{% if request|staff %}
-{{ profile.user.email }}
-{% endif %}
-
-{% trans "Talks" %}
-{% include "proposals/_talk_list.html" %}
-
-{% if request|edit_profile:profile %}
-
-{% trans "Information" %}
-
-{% trans "Travels and hosting" %}
-
-
-
- {% trans "Need transport:" %} {{ participation.need_transport|yesno:"Yes,No,Not specified" }}
- {% if participation.need_transport %}
-
- {% comment %}
- {% trans "Arrival:" %} {{ participation.arrival }}
- {% trans "Departure:" %} {{ participation.departure }}
- {% endcomment %}
- {% trans "Accepted transport means:" %} {% for transport in participation.transport.all %}{% if not forloop.first %}, {% endif %}{{ transport }}{% endfor %}
- {% trans "Departure city:" %} {{ participation.transport_city_outward|default:'Not specified' }}
- {% if participation.transport_city_return %}
- {% trans "Return city:" %} {{ participation.transport_city_return }}
- {% endif %}
- {% trans "Transport booked:" %} {{ participation.transport_booked|yesno }}
-
- {% endif %}
-
- {% trans "Need accommodation:" %} {% if participation.accommodation is None %}Not specified{% else %}{{ participation.get_accommodation_display }}{% endif %}
- {% trans "Accommodation booked:" %} {{ participation.accommodation_booked|yesno }}
-
-
-{% trans "Talk needs" %}
-
-
- {% trans "Video output:" %} {% for conn in participation.connector.all %}{% if not forloop.first %}, {% endif %}{{ conn }}{% endfor %}
- {% trans "Need sound:" %} {{ participation.sound|yesno }}
- {% trans "Ok to be recorded on video:" %} {{ participation.videotaped|yesno }}
- {% trans "Video licence:" %} {{ participation.get_video_licence_display }}
-
-
-{% trans "Constraints" %}
-{{ participation.constraints|linebreaksbr }}
-
-{% trans "Notes" %}
-{{ participation.notes|linebreaksbr }}
-
-{% endif %}
-
-
-{% endblock %}
diff --git a/accounts/templates/accounts/participant_edit.html b/accounts/templates/accounts/participant_edit.html
deleted file mode 100644
index d2eb024..0000000
--- a/accounts/templates/accounts/participant_edit.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends 'staff.html' %}
-
-{% load i18n %}
-
-{% block participantstab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-
-
{% blocktrans %}{{ profile }}'s profile{% endblocktrans %}
-
-
- {% include "_form.html" %}
-
-
-
-{% endblock %}
diff --git a/accounts/templates/accounts/participant_list.html b/accounts/templates/accounts/participant_list.html
deleted file mode 100644
index ee07385..0000000
--- a/accounts/templates/accounts/participant_list.html
+++ /dev/null
@@ -1,62 +0,0 @@
-{% extends 'staff.html' %}
-
-{% load bootstrap3 accounts_tags i18n %}
-
-{% block participantstab %} class="active"{% endblock %}
-
-{% block content %}
-
-{% trans "Participants" %}
-
-
-
- Username
- Full name
- Orga
- Reviews
- Conversations
-
- {% for participation in participation_list %}
-
- {{ participation.user.username }}
- {{ participation.user.get_full_name }}
- {{ participation.is_orga|yesno:"✔,✘" }}
- {% for topic in participation.topic_set.all %}{{ topic.get_link }}{% if not forloop.last %},
- {% endif %}{% endfor %}
-
-
- {% if request.user in participation.conversation.subscribers.all %}
-
-
- {% else %}
-
-
- {% endif %}
-
-
- {% endfor %}
-
-
-{% if request|orga %}
-{% include "_form.html" %}
-{% endif %}
-
-{% endblock %}
-
-{% block css %}
-{{ block.css }}
-{{ form.media.css }}
-{% endblock %}
-
-{% block js_end %}
-{{ block.super }}
-{{ form.media.js }}
-
-{% endblock %}
diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html
deleted file mode 100644
index 6eb8e8d..0000000
--- a/accounts/templates/accounts/profile.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends 'base.html' %}
-
-{% load bootstrap3 i18n avatar_tags %}
-
-{% block profiletab %} class="active"{% endblock %}
-
-
-{% block content %}
-
-
-
-
{% trans "Profile" %}
-
-
-
-
-{% endblock %}
-
-{% block css %}
-{{ block.super }}
-{{ form.media.css }}
-{% endblock %}
-
-{% block js_end %}
-{{ block.super }}
-{{ form.media.js }}
-
-{% endblock %}
diff --git a/accounts/templates/avatar/add.html b/accounts/templates/avatar/add.html
deleted file mode 100644
index ad0bcd3..0000000
--- a/accounts/templates/avatar/add.html
+++ /dev/null
@@ -1,14 +0,0 @@
-{% extends "avatar/base.html" %}
-{% load i18n avatar_tags %}
-
-{% block avatarcontent %}
- {% trans "Your current avatar: " %}
- {% avatar user %}
- {% if not avatars %}
- {% trans "You haven't uploaded an avatar yet. Please upload one now." %}
- {% endif %}
-
-{% endblock %}
diff --git a/accounts/templates/avatar/avatar_tag.html b/accounts/templates/avatar/avatar_tag.html
deleted file mode 100644
index 79f3e61..0000000
--- a/accounts/templates/avatar/avatar_tag.html
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/accounts/templates/avatar/base.html b/accounts/templates/avatar/base.html
deleted file mode 100644
index efe9906..0000000
--- a/accounts/templates/avatar/base.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends 'base.html' %}
-
-{% load i18n %}
-
-{% block profiletab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-
-
- {% block avatarcontent %}{% endblock %}
-
-
-
-{% endblock %}
diff --git a/accounts/templates/avatar/change.html b/accounts/templates/avatar/change.html
deleted file mode 100644
index 13755f2..0000000
--- a/accounts/templates/avatar/change.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% extends "avatar/base.html" %}
-{% load i18n avatar_tags %}
-
-{% block avatarcontent %}
- {% trans "Your current avatar: " %}
- {% avatar user %}
- {% if not avatars %}
- {% trans "You haven't uploaded an avatar yet. Please upload one now." %}
- {% else %}
-
- {% endif %}
-
-{% endblock %}
diff --git a/accounts/templates/avatar/confirm_delete.html b/accounts/templates/avatar/confirm_delete.html
deleted file mode 100644
index 8f9ad4d..0000000
--- a/accounts/templates/avatar/confirm_delete.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends "avatar/base.html" %}
-{% load i18n %}
-
-{% block avatarcontent %}
- {% trans "Please select the avatars that you would like to delete." %}
- {% if not avatars %}
- {% url 'avatar_change' as avatar_change_url %}
- {% blocktrans %}You have no avatars to delete. Please upload one now.{% endblocktrans %}
- {% else %}
-
- {% endif %}
-{% endblock %}
diff --git a/accounts/templates/registration/login.html b/accounts/templates/registration/login.html
deleted file mode 100644
index 8566b0f..0000000
--- a/accounts/templates/registration/login.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends 'base.html' %}
-
-{% load bootstrap3 i18n %}
-
-{% block logintab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- {% include '_form.html' %}
-
-
-
-
-
-
- {% url 'registration_register' as reg_url %}
- {% blocktrans %}You do not have an account yet? Please
register .{% endblocktrans %}
-
-
-
-{% endblock %}
diff --git a/accounts/templates/registration/password_reset_form.html b/accounts/templates/registration/password_reset_form.html
deleted file mode 100644
index 7290203..0000000
--- a/accounts/templates/registration/password_reset_form.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% extends 'base.html' %}
-
-{% load bootstrap3 i18n %}
-
-{% block content %}
-
-
-
-
-
-
- {% include "_form.html" %}
-
-
-
-
-{% endblock %}
diff --git a/accounts/templates/registration/registration_form.html b/accounts/templates/registration/registration_form.html
deleted file mode 100644
index 792b357..0000000
--- a/accounts/templates/registration/registration_form.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends 'base.html' %}
-
-{% load bootstrap3 i18n %}
-
-{% block registrationtab %} class="active"{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- {% include '_form.html' %}
-
-
-
-
-
-
- {% url 'login' as login_url %}
- {% blocktrans %}You already have an account? Please
login .{% endblocktrans %}
-
-
-
-{% endblock %}
diff --git a/accounts/templatetags/accounts_tags.py b/accounts/templatetags/accounts_tags.py
deleted file mode 100644
index aa19b1c..0000000
--- a/accounts/templatetags/accounts_tags.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from django import template
-
-from accounts.utils import can_edit_profile, is_orga, is_staff
-
-register = template.Library()
-
-
-@register.filter
-def orga(request):
- return is_orga(request, request.user)
-
-
-@register.filter
-def staff(request):
- return is_staff(request, request.user)
-
-
-@register.filter
-def edit_profile(request, profile):
- return can_edit_profile(request, profile)
diff --git a/accounts/tests.py b/accounts/tests.py
deleted file mode 100644
index ba43320..0000000
--- a/accounts/tests.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from django.contrib.auth.models import User
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.test import TestCase
-
-from .models import Participation, Profile
-
-ROOT_URL = 'accounts'
-
-
-class AccountTests(TestCase):
- def setUp(self):
- a, b, c = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abc')
- Participation.objects.create(user=a, site=Site.objects.first())
- Participation.objects.create(user=b, site=Site.objects.first())
- Participation.objects.create(user=c, site=Site.objects.first(), orga=True)
-
- def test_models(self):
- self.assertEqual(Profile.objects.count(), 3)
- self.client.login(username='c', password='c')
- for model in [Profile, Participation]:
- item = model.objects.first()
- self.assertEqual(self.client.get(item.full_link()).status_code, 200)
- self.assertTrue(str(item))
-
- def test_views(self):
- # User b wants to update its username, email and biography
- user = User.objects.get(username='b')
- self.assertEqual(user.email, 'b@example.org')
- self.assertEqual(user.profile.biography, '')
-
- self.client.login(username='b', password='b')
-
- # He tries with an invalid address
- self.client.post(reverse('profile'), {'email': 'bnewdomain.com', 'username': 'z', 'biography': 'tester',
- 'video_licence': 1})
- self.assertEqual(User.objects.filter(username='z').count(), 0)
-
- self.client.post(reverse('profile'), {'email': 'b@newdomain.com', 'username': 'z', 'biography': 'tester',
- 'video_licence': 1})
-
- user = User.objects.get(username='z')
- self.assertEqual(user.email, 'b@newdomain.com')
- self.assertEqual(user.profile.biography, 'tester')
- self.client.logout()
-
- def test_participant_views(self):
- self.assertEqual(self.client.get(reverse('registration_register')).status_code, 200)
- self.client.login(username='b', password='b')
- self.assertEqual(self.client.get(reverse('list-participants')).status_code, 403)
- self.assertEqual(self.client.post(reverse('edit-participant', kwargs={'username': 'a'}),
- {'biography': 'foo'}).status_code, 403)
- b = User.objects.get(username='b')
- b.is_superuser = True
- b.save()
- p = Participation.objects.get(user=b)
- self.assertFalse(p.orga)
- self.assertEqual(self.client.get(reverse('list-participants')).status_code, 403)
- # login signal should set orga to True due to superuser status
- self.client.login(username='b', password='b')
- p = Participation.objects.get(user=b)
- self.assertTrue(p.orga)
- self.assertEqual(self.client.get(reverse('list-participants')).status_code, 200)
- self.assertEqual(self.client.post(reverse('edit-participant', kwargs={'username': 'a'}),
- {'biography': 'foo', 'nootes': 'bar'}).status_code, 200)
- self.assertEqual(User.objects.get(username='a').profile.biography, '')
- self.assertEqual(self.client.post(reverse('edit-participant', kwargs={'username': 'a'}),
- {'biography': 'foo', 'notes': 'bar', 'first_name': 'Jules', 'username': 'a',
- 'last_name': 'César', 'email': 'a@example.org', 'transport': 1,
- 'connector': 1, 'video_licence': 2, 'constraints': 'nope', 'orga': 0,
- }).status_code, 200)
- self.assertEqual(User.objects.get(username='a').profile.biography, 'foo')
- self.assertEqual(Participation.objects.get(user=User.objects.get(username='a')).video_licence, 2)
-
-
-from datetime import datetime
-from .models import AvailabilityTimeslot
-class DisponibilitiesTests(TestCase):
-
- def setUp(self):
- self.user = User.objects.create_user('a', email='a@example.org', password='a')
- self.participation = Participation.objects.create(user=self.user, site=Site.objects.first())
-
- def test_is_available(self):
- from django.utils.timezone import is_naive, get_default_timezone
- tz = get_default_timezone()
- d = {}
- for i in range(8, 18, 1):
- d[i] = datetime(2016, 10, 10, i, 0, 0, tzinfo=tz)
- self.assertEquals(self.participation.is_available(d[10]), None)
- AvailabilityTimeslot.objects.create(participation=self.participation, start=d[10], end=d[12])
- self.assertEquals(self.participation.is_available(d[9]), False)
- self.assertEquals(self.participation.is_available(d[11]), True)
- self.assertEquals(self.participation.is_available(d[13]), False)
- self.assertEquals(self.participation.is_available(d[8], d[9]), False)
- self.assertEquals(self.participation.is_available(d[9], d[11]), False)
- self.assertEquals(self.participation.is_available(d[10], d[11]), True)
- self.assertEquals(self.participation.is_available(d[11], d[12]), True)
- self.assertEquals(self.participation.is_available(d[10], d[12]), True)
- self.assertEquals(self.participation.is_available(d[11], d[13]), False)
- self.assertEquals(self.participation.is_available(d[13], d[14]), False)
- AvailabilityTimeslot.objects.create(participation=self.participation, start=d[14], end=d[16])
- self.assertEquals(self.participation.is_available(d[10], d[12]), True)
- self.assertEquals(self.participation.is_available(d[14], d[16]), True)
- self.assertEquals(self.participation.is_available(d[11], d[15]), False)
- self.assertEquals(self.participation.is_available(d[11], d[17]), False)
- self.assertEquals(self.participation.is_available(d[13], d[17]), False)
- self.assertEquals(self.participation.is_available(d[9], d[15]), False)
diff --git a/accounts/urls.py b/accounts/urls.py
deleted file mode 100644
index 4f722ea..0000000
--- a/accounts/urls.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from django.conf import settings
-from django.conf.urls import include, url
-from django.contrib.auth import views as auth_views
-
-from . import views
-
-urlpatterns = [
- url(r'^profile/$', views.profile, name='profile'),
- url(r'^login/$', auth_views.login, {'extra_context': {'buttons': [views.RESET_PASSWORD_BUTTON]}}, name='login'),
- url(r'^logout/$', auth_views.logout, {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'),
- url(r'^participant/$', views.participation_list, name='list-participants'),
- url(r'^participant/(?P[\w.@+-]+)$', views.participant_details, name='show-participant'),
- url(r'^participant/(?P[\w.@+-]+)/edit/$', views.participant_edit, name='edit-participant'),
- url(r'^avatar/', include('avatar.urls')),
- url(r'', include('django.contrib.auth.urls')),
- url(r'', include('registration.backends.default.urls')),
-]
diff --git a/accounts/utils.py b/accounts/utils.py
deleted file mode 100644
index 8854676..0000000
--- a/accounts/utils.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from django.contrib.sites.shortcuts import get_current_site
-from django.utils.crypto import get_random_string
-
-
-def generate_user_uid():
- return get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')
-
-
-def is_orga(request, user):
- return user.is_authenticated and user.participation_set.get(site=get_current_site(request)).is_orga()
-
-
-def is_staff(request, user):
- return user.is_authenticated and user.participation_set.get(site=get_current_site(request)).is_staff()
-
-
-def can_edit_profile(request, profile):
- editor = request.user.participation_set.get(site=get_current_site(request))
- return editor.is_orga() or editor.topic_set.filter(talk__speakers=profile.user).exists()
diff --git a/accounts/views.py b/accounts/views.py
deleted file mode 100644
index da2bfd6..0000000
--- a/accounts/views.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required
-from django.contrib.sites.shortcuts import get_current_site
-from django.core.exceptions import PermissionDenied
-from django.core.urlresolvers import reverse
-from django.shortcuts import get_object_or_404, redirect, render
-from django.utils.translation import ugettext_lazy as _
-
-from .decorators import staff_required
-from .forms import (NewParticipationForm, ParticipationForm,
- ParticipationOrgaForm, ProfileForm, ProfileOrgaForm, UserForm)
-from .models import Participation, Profile, User
-from .utils import can_edit_profile, is_orga
-
-from proposals.models import Talk
-from proposals.utils import allowed_talks
-
-RESET_PASSWORD_BUTTON = ('password_reset', 'warning', _('Reset your password'))
-CHANGE_PASSWORD_BUTTON = ('password_change', 'warning', _('Change password'))
-CHANGE_AVATAR_BUTTON = ('avatar_change', 'default', _('Change avatar'))
-
-
-@login_required
-def profile(request):
-
- user_form = UserForm(request.POST or None, instance=request.user)
- profile_form = ProfileForm(request.POST or None, instance=request.user.profile)
- participation_form = ParticipationForm(request.POST or None, instance=Participation.objects.get(site=get_current_site(request),
- user=request.user))
- forms = [user_form, profile_form, participation_form]
-
- if request.method == 'POST':
- if all(form.is_valid() for form in forms):
- for form in forms:
- form.save()
- messages.success(request, _('Profile updated successfully.'))
- else:
- messages.error(request, _('Please correct those errors.'))
-
- return render(request, 'accounts/profile.html', {
- 'user_form': user_form,
- 'profile_form': profile_form,
- 'participation_form': participation_form,
- 'buttons': [CHANGE_PASSWORD_BUTTON]
- })
-
-
-@login_required
-def participant_edit(request, username):
-
- profile = get_object_or_404(Profile, user__username=username)
- if not can_edit_profile(request, profile):
- raise PermissionDenied()
-
- participation_form = ParticipationOrgaForm if is_orga(request, request.user) else ParticipationForm
- forms = [UserForm(request.POST or None, instance=profile.user),
- ProfileOrgaForm(request.POST or None, instance=profile),
- participation_form(request.POST or None,
- instance=Participation.objects.get(site=get_current_site(request), user=profile.user))]
-
- if request.method == 'POST':
- if all(form.is_valid() for form in forms):
- for form in forms:
- form.save()
- messages.success(request, _('Profile updated successfully.'))
- else:
- messages.error(request, _('Please correct those errors.'))
-
- return render(request, 'accounts/participant_edit.html', {'forms': forms, 'profile': profile})
-
-
-@staff_required
-def participation_list(request):
- participation_list = Participation.objects.filter(site=get_current_site(request)).all()
- form = NewParticipationForm(request.POST or None, site=get_current_site(request))
-
- if request.method == 'POST' and form.is_valid():
- if not Participation.objects.get(user=request.user, site=get_current_site(request)).is_orga():
- raise PermissionDenied()
- user = User.objects.get(username=form.cleaned_data['participant'])
- participation, created = Participation.objects.get_or_create(user=user, site=get_current_site(request))
- if created:
- messages.success(request, _("%(name)s added to participants") % {'name': user.profile})
- else:
- messages.info(request, _("%(name)s is already a participant") % {'name': user.profile})
- return redirect(reverse('list-participants'))
-
- return render(request, 'accounts/participant_list.html', {
- 'participation_list': participation_list,
- 'form': form,
- })
-
-
-@login_required
-def participant_details(request, username):
- user = get_object_or_404(User, username=username)
- participation = get_object_or_404(Participation, user=user, site=get_current_site(request))
- return render(request, 'accounts/participant_details.html', {
- 'profile': user.profile,
- 'participation': participation,
- 'talk_list': allowed_talks(Talk.objects.filter(site=get_current_site(request), speakers=user), request),
- })
diff --git a/cfp/__init__.py b/cfp/__init__.py
new file mode 100644
index 0000000..9d019f2
--- /dev/null
+++ b/cfp/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'cfp.apps.CFPConfig'
diff --git a/cfp/admin.py b/cfp/admin.py
new file mode 100644
index 0000000..dc2d484
--- /dev/null
+++ b/cfp/admin.py
@@ -0,0 +1,43 @@
+from django.contrib import admin
+from django.contrib.sites.models import Site
+
+from ponyconf.admin import SiteAdminMixin
+from .models import Conference, Participant, Talk, TalkCategory, Track, Vote
+
+
+class ConferenceAdmin(SiteAdminMixin, admin.ModelAdmin):
+ filter_horizontal = ('staff',)
+
+ def has_add_permission(self, request):
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+
+class ParticipantAdmin(SiteAdminMixin, admin.ModelAdmin):
+ pass
+
+
+class TrackAdmin(SiteAdminMixin, admin.ModelAdmin):
+ pass
+
+
+class TalkCategoryAdmin(SiteAdminMixin, admin.ModelAdmin):
+ pass
+
+
+class TalkAdmin(SiteAdminMixin, admin.ModelAdmin):
+ def get_form(self, request, obj=None, **kwargs):
+ form = super().get_form(request, obj, **kwargs)
+ form.base_fields['speakers'].queryset = Participant.objects.filter(site=request.conference.site)
+ form.base_fields['track'].queryset = Track.objects.filter(site=request.conference.site)
+ form.base_fields['category'].queryset = TalkCategory.objects.filter(site=request.conference.site)
+ return form
+
+
+admin.site.register(Conference, ConferenceAdmin)
+admin.site.register(Participant, ParticipantAdmin)
+admin.site.register(Talk, TalkAdmin)
+admin.site.register(TalkCategory, TalkCategoryAdmin)
+admin.site.register(Vote)
diff --git a/cfp/apps.py b/cfp/apps.py
new file mode 100644
index 0000000..e9f8694
--- /dev/null
+++ b/cfp/apps.py
@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+from django.db.models.signals import post_migrate
+
+
+class CFPConfig(AppConfig):
+ name = 'cfp'
+
+ def ready(self):
+ import cfp.signals # noqa
+ post_migrate.connect(cfp.signals.call_first_site_post_save, sender=self)
diff --git a/cfp/context_processors.py b/cfp/context_processors.py
new file mode 100644
index 0000000..4a24df3
--- /dev/null
+++ b/cfp/context_processors.py
@@ -0,0 +1,4 @@
+
+
+def conference(request):
+ return {'conference': request.conference}
diff --git a/accounts/decorators.py b/cfp/decorators.py
similarity index 55%
rename from accounts/decorators.py
rename to cfp/decorators.py
index 027a5b6..a5ca0ac 100644
--- a/accounts/decorators.py
+++ b/cfp/decorators.py
@@ -3,18 +3,7 @@ from functools import wraps
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
-from accounts.utils import is_orga, is_staff
-
-
-def orga_required(view_func):
- def _is_orga(request, *args, **kwargs):
- if not request.user.is_authenticated():
- return login_required(view_func)(request, *args, **kwargs)
- elif is_orga(request, request.user):
- return view_func(request, *args, **kwargs)
- else:
- raise PermissionDenied
- return wraps(view_func)(_is_orga)
+from cfp.utils import is_staff
def staff_required(view_func):
diff --git a/cfp/forms.py b/cfp/forms.py
new file mode 100644
index 0000000..efb5122
--- /dev/null
+++ b/cfp/forms.py
@@ -0,0 +1,191 @@
+from django import forms
+from django.forms.models import modelform_factory
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.models import User
+from django.contrib.auth.forms import UsernameField
+from django.utils.translation import ugettext_lazy as _
+from django.template.defaultfilters import slugify
+from django.utils.crypto import get_random_string
+
+from django_select2.forms import ModelSelect2MultipleWidget
+
+from .models import Participant, Talk, TalkCategory, Track, Conference, Room
+
+
+STATUS_CHOICES = [
+ ('pending', _('Pending decision')),
+ ('accepted', _('Accepted')),
+ ('declined', _('Declined')),
+]
+STATUS_VALUES = [
+ ('pending', None),
+ ('accepted', True),
+ ('declined', False),
+]
+
+
+class TalkForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ categories = kwargs.pop('categories')
+ super().__init__(*args, **kwargs)
+ if categories.exists():
+ self.fields['category'].queryset = categories
+ else:
+ del self.fields['category']
+
+ class Meta:
+ model = Talk
+ fields = ('category', 'title', 'description','notes')
+
+
+class TalkStaffForm(TalkForm):
+ def __init__(self, *args, **kwargs):
+ tracks = kwargs.pop('tracks')
+ super().__init__(*args, **kwargs)
+ self.fields['track'].queryset = tracks
+
+ class Meta(TalkForm.Meta):
+ fields = ('category', 'track', 'title', 'description', 'notes', 'start_date', 'duration', 'room',)
+ labels = {
+ 'category': _('Category'),
+ 'title': _('Title'),
+ 'description': _('Description'),
+ 'notes': _('Notes'),
+ }
+ help_texts = {
+ 'notes': _('Visible by speakers'),
+ }
+
+
+class TalkFilterForm(forms.Form):
+ category = forms.MultipleChoiceField(
+ label=_('Category'),
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=[],
+ )
+ status = forms.MultipleChoiceField(
+ label=_('Status'),
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=STATUS_CHOICES,
+ )
+ track = forms.MultipleChoiceField(
+ label=_('Track'),
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=[],
+ )
+ vote = forms.NullBooleanField(
+ label=_('Vote'),
+ help_text=_('Filter talks you already / not yet voted for'),
+ )
+ room = forms.NullBooleanField(
+ label=_('Room'),
+ help_text=_('Filter talks already / not yet affected to a room'),
+ )
+ scheduled = forms.NullBooleanField(
+ label=_('Scheduled'),
+ help_text=_('Filter talks already / not yet scheduled'),
+ )
+
+ def __init__(self, *args, **kwargs):
+ site = kwargs.pop('site')
+ super().__init__(*args, **kwargs)
+ categories = TalkCategory.objects.filter(site=site)
+ self.fields['category'].choices = categories.values_list('pk', 'name')
+ tracks = Track.objects.filter(site=site)
+ self.fields['track'].choices = [('none', _('Not assigned'))] + list(tracks.values_list('slug', 'name'))
+
+
+ParticipantForm = modelform_factory(Participant, fields=('name', 'email', 'biography'))
+
+
+class ParticipantStaffForm(ParticipantForm):
+ class Meta(ParticipantForm.Meta):
+ labels = {
+ 'name': _('Name'),
+ }
+
+
+class UsersWidget(ModelSelect2MultipleWidget):
+ model = User
+ search_fields = [ '%s__icontains' % field for field in UserAdmin.search_fields ]
+
+
+class ConferenceForm(forms.ModelForm):
+ class Meta:
+ model = Conference
+ fields = ['name', 'home', 'venue', 'city', 'contact_email', 'reply_email', 'secure_domain', 'staff',]
+ widgets = {
+ 'staff': UsersWidget(),
+ }
+ help_texts = {
+ 'staff': _('New staff members will be informed of their new position by e-mail.'),
+ }
+
+
+class CreateUserForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ("first_name", "last_name", "email")
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['first_name'].required = True
+ self.fields['last_name'].required = True
+ self.fields['email'].required = True
+
+ def clean(self):
+ super().clean()
+ user = User(first_name=self.cleaned_data.get('first_name'), last_name=self.cleaned_data.get('last_name'))
+ username = slugify(user.get_full_name())
+ if User.objects.filter(username=username).exists():
+ raise forms.ValidationError(_('An user with that firstname and that lastname already exists.'))
+
+ def clean_email(self):
+ email = self.cleaned_data.get('email')
+ if email and User.objects.filter(email=email).exists():
+ raise forms.ValidationError(_('A user with that email already exists.'))
+ return email
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.username = slugify(user.get_full_name())
+ user.set_password(get_random_string(length=32))
+ if commit:
+ user.save()
+ 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 Track.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
+ fields = ['name', 'description']
+
+
+class RoomForm(OnSiteNamedModelForm):
+ class Meta:
+ model = Room
+ fields = ['name', 'label', 'capacity']
diff --git a/cfp/middleware.py b/cfp/middleware.py
new file mode 100644
index 0000000..61501dc
--- /dev/null
+++ b/cfp/middleware.py
@@ -0,0 +1,16 @@
+from django.contrib.sites.shortcuts import get_current_site
+
+from .models import Conference
+
+
+class ConferenceMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ return self.get_response(request)
+
+ def process_view(self, request, view, view_args, view_kwargs):
+ site = get_current_site(request)
+ conf = Conference.objects.select_related('site').prefetch_related('staff').get(site=site)
+ request.conference = conf
diff --git a/proposals/migrations/0001_initial.py b/cfp/migrations/0001_initial.py
similarity index 58%
rename from proposals/migrations/0001_initial.py
rename to cfp/migrations/0001_initial.py
index 8cb8222..9d3376d 100644
--- a/proposals/migrations/0001_initial.py
+++ b/cfp/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-01-13 10:49
+# Generated by Django 1.11 on 2017-06-06 22:14
from __future__ import unicode_literals
import autoslug.fields
@@ -8,7 +8,7 @@ from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
-import proposals.models
+import uuid
class Migration(migrations.Migration):
@@ -16,39 +16,72 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('planning', '0001_initial'),
('sites', '0002_alter_domain_unique'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
- migrations.CreateModel(
- name='Attendee',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('name', models.CharField(blank=True, default='', max_length=64)),
- ('email', models.EmailField(blank=True, default='', max_length=254)),
- ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'abstract': False,
- },
- ),
migrations.CreateModel(
name='Conference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, max_length=100)),
('home', models.TextField(blank=True, default='')),
('venue', models.TextField(blank=True, default='')),
('city', models.CharField(blank=True, default='', max_length=64)),
- ('subscriptions_open', models.BooleanField(default=False)),
+ ('contact_email', models.CharField(blank=True, max_length=100)),
+ ('custom_css', models.TextField(blank=True)),
+ ('external_css_link', models.URLField(blank=True)),
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
migrations.CreateModel(
- name='Event',
+ name='Participant',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('name', models.CharField(max_length=128, verbose_name='Your Name')),
+ ('email', models.EmailField(max_length=254)),
+ ('biography', models.TextField(verbose_name='Biography')),
+ ('token', models.UUIDField(default=uuid.uuid4, editable=False)),
+ ('twitter', models.CharField(blank=True, default='', max_length=100, verbose_name='Twitter')),
+ ('linkedin', models.CharField(blank=True, default='', max_length=100, verbose_name='LinkedIn')),
+ ('github', models.CharField(blank=True, default='', max_length=100, verbose_name='Github')),
+ ('website', models.CharField(blank=True, default='', max_length=100, verbose_name='Website')),
+ ('facebook', models.CharField(blank=True, default='', max_length=100, verbose_name='Facebook')),
+ ('mastodon', models.CharField(blank=True, default='', max_length=100, verbose_name='Mastodon')),
+ ('phone_number', models.CharField(blank=True, default='', max_length=64, verbose_name='Phone number')),
+ ('language', models.CharField(blank=True, max_length=10)),
+ ('notes', models.TextField(blank=True, default='', help_text='This field is only visible by organizers.', verbose_name='Notes')),
+ ('vip', models.BooleanField(default=False)),
+ ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Talk',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('updated', models.DateTimeField(auto_now=True)),
+ ('title', models.CharField(max_length=128, verbose_name='Talk Title')),
+ ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
+ ('description', models.TextField(verbose_name='Description of your talk')),
+ ('notes', models.TextField(blank=True, help_text='If you have any constraint or if you have anything that may help you to select your talk, like a video or slides of your talk, please write it down here', verbose_name='Message to organizers')),
+ ('videotaped', models.BooleanField(default=True, verbose_name="I'm ok to be recorded on video")),
+ ('video_licence', models.CharField(choices=[('CC-Zero CC-BY', 'CC-Zero CC-BY'), ('CC-BY-SA', 'CC-BY-SA'), ('CC-BY-ND', 'CC-BY-ND'), ('CC-BY-NC', 'CC-BY-NC'), ('CC-BY-NC-SA', 'CC-BY-NC-SA'), ('CC-BY-NC-ND', 'CC-BY-NC-ND')], default='CC-BY-SA', max_length=10, verbose_name='Video licence')),
+ ('sound', models.BooleanField(default=False, verbose_name='I need sound')),
+ ('accepted', models.NullBooleanField(default=None)),
+ ('duration', models.PositiveIntegerField(default=0, verbose_name='Duration (min)')),
+ ('plenary', models.BooleanField(default=False)),
+ ('token', models.UUIDField(default=uuid.uuid4, editable=False)),
+ ],
+ options={
+ 'ordering': ('category__id', 'title'),
+ },
+ ),
+ migrations.CreateModel(
+ name='TalkCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
@@ -60,51 +93,11 @@ class Migration(migrations.Migration):
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
options={
+ 'verbose_name': 'category',
+ 'verbose_name_plural': 'categories',
'ordering': ('pk',),
},
),
- migrations.CreateModel(
- name='Talk',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('title', models.CharField(help_text='After submission, title can only be changed by the staff.', max_length=128, verbose_name='Title')),
- ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
- ('abstract', models.CharField(blank=True, max_length=255, verbose_name='Abstract')),
- ('description', models.TextField(blank=True, verbose_name='Description')),
- ('notes', models.TextField(blank=True, verbose_name='Notes')),
- ('accepted', models.NullBooleanField(default=None)),
- ('start_date', models.DateTimeField(blank=True, default=None, null=True)),
- ('duration', models.PositiveIntegerField(default=0, verbose_name='Duration (min)')),
- ('plenary', models.BooleanField(default=False)),
- ('registration_required', models.BooleanField(default=False)),
- ('attendees_limit', models.PositiveIntegerField(default=0, verbose_name='Max. number of attendees')),
- ('materials', models.FileField(help_text='You can use this field to share some materials related to your intervention.', null=True, upload_to=proposals.models.talk_materials_destination, verbose_name='Materials')),
- ('attendees', models.ManyToManyField(to='proposals.Attendee', verbose_name='Attendees')),
- ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Event', verbose_name='Intervention kind')),
- ('proposer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
- ('room', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='planning.Room')),
- ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
- ('speakers', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Speakers')),
- ],
- options={
- 'ordering': ('event__id',),
- },
- ),
- migrations.CreateModel(
- name='Topic',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('name', models.CharField(max_length=128, verbose_name='Name')),
- ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)),
- ('description', models.TextField(blank=True, verbose_name='Description')),
- ('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
- ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
- ],
- ),
migrations.CreateModel(
name='Track',
fields=[
@@ -114,7 +107,6 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name')),
('description', models.TextField(blank=True, verbose_name='Description')),
- ('managers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Managers')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
@@ -125,24 +117,29 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('vote', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(-2), django.core.validators.MaxValueValidator(2)])),
- ('talk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Talk')),
+ ('talk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cfp.Talk')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
- model_name='topic',
- name='track',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='proposals.Track', verbose_name='Destination track'),
+ model_name='talk',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cfp.TalkCategory', verbose_name='Talk Category'),
),
migrations.AddField(
model_name='talk',
- name='topics',
- field=models.ManyToManyField(blank=True, help_text='The topics can not be changed after submission.', to='proposals.Topic', verbose_name='Topics'),
+ name='site',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
+ ),
+ migrations.AddField(
+ model_name='talk',
+ name='speakers',
+ field=models.ManyToManyField(to='cfp.Participant', verbose_name='Speakers'),
),
migrations.AddField(
model_name='talk',
name='track',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='proposals.Track', verbose_name='Track'),
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cfp.Track', verbose_name='Track'),
),
migrations.AlterUniqueTogether(
name='vote',
@@ -153,11 +150,11 @@ class Migration(migrations.Migration):
unique_together=set([('site', 'name')]),
),
migrations.AlterUniqueTogether(
- name='topic',
+ name='talkcategory',
unique_together=set([('site', 'name')]),
),
migrations.AlterUniqueTogether(
- name='event',
- unique_together=set([('site', 'name')]),
+ name='participant',
+ unique_together=set([('site', 'email')]),
),
]
diff --git a/cfp/migrations/0002_conference_staff.py b/cfp/migrations/0002_conference_staff.py
new file mode 100644
index 0000000..ddc6e91
--- /dev/null
+++ b/cfp/migrations/0002_conference_staff.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-07-29 23:30
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('cfp', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='conference',
+ name='staff',
+ field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Staff'),
+ ),
+ ]
diff --git a/cfp/migrations/0003_auto_20170801_1400.py b/cfp/migrations/0003_auto_20170801_1400.py
new file mode 100644
index 0000000..8fc79c3
--- /dev/null
+++ b/cfp/migrations/0003_auto_20170801_1400.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-01 14:00
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cfp', '0002_conference_staff'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='conference',
+ name='city',
+ field=models.CharField(blank=True, default='', max_length=64, verbose_name='City'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='contact_email',
+ field=models.CharField(blank=True, max_length=100, verbose_name='Contact email'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='home',
+ field=models.TextField(blank=True, default='', verbose_name='Homepage (markdown)'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='name',
+ field=models.CharField(blank=True, max_length=100, verbose_name='Conference name'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='staff',
+ field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Staff members'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='venue',
+ field=models.TextField(blank=True, default='', verbose_name='Venue information'),
+ ),
+ ]
diff --git a/cfp/migrations/0004_auto_20170801_1408.py b/cfp/migrations/0004_auto_20170801_1408.py
new file mode 100644
index 0000000..62c3be9
--- /dev/null
+++ b/cfp/migrations/0004_auto_20170801_1408.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-01 14:08
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def generate_participant_conversation(apps, schema_editor):
+ MessageThread = apps.get_model("mailing", "MessageThread")
+ Participant = apps.get_model("cfp", "Participant")
+ db_alias = schema_editor.connection.alias
+ for participant in Participant.objects.using(db_alias).filter(conversation=None):
+ participant.conversation = MessageThread.objects.create()
+ participant.save()
+
+
+def generate_talk_conversation(apps, schema_editor):
+ MessageThread = apps.get_model("mailing", "MessageThread")
+ Talk = apps.get_model("cfp", "Talk")
+ db_alias = schema_editor.connection.alias
+ for talk in Talk.objects.using(db_alias).filter(conversation=None):
+ talk.conversation = MessageThread.objects.create()
+ talk.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mailing', '0001_initial'),
+ ('cfp', '0003_auto_20170801_1400'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='participant',
+ name='conversation',
+ field=models.OneToOneField(null=True, default=None, on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
+ preserve_default=False,
+ ),
+ migrations.RunPython(generate_participant_conversation),
+ migrations.AlterField(
+ model_name='participant',
+ name='conversation',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
+ ),
+ migrations.AddField(
+ model_name='talk',
+ name='conversation',
+ field=models.OneToOneField(null=True, default=None, on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
+ preserve_default=False,
+ ),
+ migrations.RunPython(generate_talk_conversation),
+ migrations.AlterField(
+ model_name='talk',
+ name='conversation',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
+ ),
+ ]
diff --git a/cfp/migrations/0005_conference_reply_email.py b/cfp/migrations/0005_conference_reply_email.py
new file mode 100644
index 0000000..c69c183
--- /dev/null
+++ b/cfp/migrations/0005_conference_reply_email.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-01 16:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cfp', '0004_auto_20170801_1408'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='conference',
+ name='reply_email',
+ field=models.CharField(blank=True, max_length=100, verbose_name='Reply email'),
+ ),
+ ]
diff --git a/cfp/migrations/0006_auto_20170811_1457.py b/cfp/migrations/0006_auto_20170811_1457.py
new file mode 100644
index 0000000..f084e8c
--- /dev/null
+++ b/cfp/migrations/0006_auto_20170811_1457.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-11 14:57
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cfp', '0005_conference_reply_email'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='participant',
+ name='token',
+ field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+ ),
+ migrations.AlterField(
+ model_name='talk',
+ name='token',
+ field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+ ),
+ ]
diff --git a/cfp/migrations/0007_conference_secure_domain.py b/cfp/migrations/0007_conference_secure_domain.py
new file mode 100644
index 0000000..66e9705
--- /dev/null
+++ b/cfp/migrations/0007_conference_secure_domain.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-11 21:18
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('cfp', '0006_auto_20170811_1457'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='conference',
+ name='secure_domain',
+ field=models.BooleanField(default=True, verbose_name='Secure domain'),
+ ),
+ ]
diff --git a/planning/migrations/0001_initial.py b/cfp/migrations/0008_auto_20170811_2342.py
similarity index 60%
rename from planning/migrations/0001_initial.py
rename to cfp/migrations/0008_auto_20170811_2342.py
index fff5ab1..62effac 100644
--- a/planning/migrations/0001_initial.py
+++ b/cfp/migrations/0008_auto_20170811_2342.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-01-13 10:49
+# Generated by Django 1.11.3 on 2017-08-11 23:42
from __future__ import unicode_literals
import autoslug.fields
@@ -9,10 +9,9 @@ import django.db.models.deletion
class Migration(migrations.Migration):
- initial = True
-
dependencies = [
('sites', '0002_alter_domain_unique'),
+ ('cfp', '0007_conference_secure_domain'),
]
operations = [
@@ -30,6 +29,21 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
+ migrations.AddField(
+ model_name='talk',
+ name='start_date',
+ field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Beginning date and time'),
+ ),
+ migrations.AlterField(
+ model_name='conference',
+ name='secure_domain',
+ field=models.BooleanField(default=True, verbose_name='Secure domain (HTTPS)'),
+ ),
+ migrations.AddField(
+ model_name='talk',
+ name='room',
+ field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='cfp.Room'),
+ ),
migrations.AlterUniqueTogether(
name='room',
unique_together=set([('site', 'name')]),
diff --git a/accounts/migrations/__init__.py b/cfp/migrations/__init__.py
similarity index 100%
rename from accounts/migrations/__init__.py
rename to cfp/migrations/__init__.py
diff --git a/cfp/mixins.py b/cfp/mixins.py
new file mode 100644
index 0000000..a916d61
--- /dev/null
+++ b/cfp/mixins.py
@@ -0,0 +1,13 @@
+from django.contrib.auth.mixins import UserPassesTestMixin
+
+from .utils import is_staff
+
+
+class StaffRequiredMixin(UserPassesTestMixin):
+ def test_func(self):
+ return is_staff(self.request, self.request.user)
+
+
+class OnSiteMixin:
+ def get_queryset(self):
+ return super().get_queryset().filter(site=self.request.conference.site)
diff --git a/cfp/models.py b/cfp/models.py
new file mode 100644
index 0000000..cd72dc9
--- /dev/null
+++ b/cfp/models.py
@@ -0,0 +1,347 @@
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.db.models import Q, Sum, Avg, Case, When
+from django.db.models.functions import Coalesce
+from django.utils import timezone
+from django.utils.translation import ugettext, ugettext_lazy as _
+
+from autoslug import AutoSlugField
+from colorful.fields import RGBColorField
+
+import uuid
+from datetime import timedelta
+
+from ponyconf.utils import PonyConfModel
+from mailing.models import MessageThread
+
+
+
+class Conference(models.Model):
+
+ site = models.OneToOneField(Site, on_delete=models.CASCADE)
+ name = models.CharField(blank=True, max_length=100, verbose_name=_('Conference name'))
+ home = models.TextField(blank=True, default="", verbose_name=_('Homepage (markdown)'))
+ venue = models.TextField(blank=True, default="", verbose_name=_('Venue information'))
+ city = models.CharField(max_length=64, blank=True, default="", verbose_name=_('City'))
+ contact_email = models.CharField(max_length=100, blank=True, verbose_name=_('Contact email'))
+ 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)'))
+
+ custom_css = models.TextField(blank=True)
+ external_css_link = models.URLField(blank=True)
+
+ #subscriptions_open = models.BooleanField(default=False) # workshop subscription
+
+ #def cfp_is_open(self):
+ # events = Event.objects.filter(site=self.site)
+ # return any(map(lambda x: x.is_open(), events))
+
+ @property
+ def opened_categories(self):
+ now = timezone.now()
+ return TalkCategory.objects.filter(site=self.site)\
+ .filter(Q(opening_date__isnull=True) | Q(opening_date__lte=now))\
+ .filter(Q(closing_date__isnull=True) | Q(closing_date__gte=now))
+
+ def from_email(self):
+ return self.name+' <'+self.contact_email+'>'
+
+ def clean_fields(self, exclude=None):
+ super().clean_fields(exclude)
+ if self.reply_email is not None:
+ try:
+ self.reply_email.format(token='a' * 80)
+ except Exception:
+ raise ValidationError({
+ 'reply_email': _('The reply email should be a formatable string accepting a token argument (e.g. ponyconf+{token}@exemple.com).'),
+ })
+
+ def __str__(self):
+ return str(self.site)
+
+
+class ParticipantManager(models.Manager):
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = qs.annotate(
+ accepted_talk_count=Sum(Case(When(talk__accepted=True, then=1), default=0, output_field=models.IntegerField())),
+ pending_talk_count=Sum(Case(When(talk__accepted=None, then=1), default=0, output_field=models.IntegerField())),
+ refused_talk_count=Sum(Case(When(talk__accepted=False, then=1), default=0, output_field=models.IntegerField())),
+ )
+ return qs
+
+
+class Participant(PonyConfModel):
+
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+
+ name = models.CharField(max_length=128, verbose_name=_('Your Name'))
+ email = models.EmailField()
+
+ biography = models.TextField(verbose_name=_('Biography'))
+ token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
+
+ twitter = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Twitter'))
+ linkedin = models.CharField(max_length=100, blank=True, default='', verbose_name=_('LinkedIn'))
+ github = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Github'))
+ website = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Website'))
+ facebook = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Facebook'))
+ mastodon = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Mastodon'))
+
+ phone_number = models.CharField(max_length=64, blank=True, default='', verbose_name=_('Phone number'))
+
+ language = models.CharField(max_length=10, blank=True)
+
+ notes = models.TextField(default='', blank=True, verbose_name=_("Notes"), help_text=_('This field is only visible by organizers.'))
+
+ vip = models.BooleanField(default=False)
+
+ conversation = models.OneToOneField(MessageThread)
+
+ objects = ParticipantManager()
+
+ def get_absolute_url(self):
+ return reverse('participant-details', kwargs={'participant_id': self.token})
+
+ class Meta:
+ # A User can participe only once to a Conference (= Site)
+ unique_together = ('site', 'email')
+
+ def __str__(self):
+ return str(self.name)
+
+ @property
+ def accepted_talk_set(self):
+ return self.talk_set.filter(accepted=True)
+ @property
+ def pending_talk_set(self):
+ return self.talk_set.filter(accepted=None)
+ @property
+ def refused_talk_set(self):
+ return self.talk_set.filter(accepted=False)
+
+
+class Track(PonyConfModel):
+
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+
+ name = models.CharField(max_length=128, verbose_name=_('Name'))
+ slug = AutoSlugField(populate_from='name')
+ description = models.TextField(blank=True, verbose_name=_('Description'))
+
+ #managers = models.ManyToManyField(User, blank=True, verbose_name=_('Managers'))
+
+ class Meta:
+ unique_together = ('site', 'name')
+
+ def estimated_duration(self):
+ return sum([talk.estimated_duration for talk in self.talk_set.all()])
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('talk-list') + '?track=%s' % self.slug
+
+
+class Room(models.Model):
+
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+ slug = AutoSlugField(populate_from='name')
+ name = models.CharField(max_length=256, blank=True, default="")
+ label = models.CharField(max_length=256, blank=True, default="")
+ capacity = models.IntegerField(default=0)
+
+ class Meta:
+ unique_together = ['site', 'name']
+ ordering = ['name']
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse('room-details', kwargs={'slug': self.slug})
+
+ @property
+ def talks(self):
+ return self.talk_set.exclude(accepted=False)
+
+ @property
+ def talks_by_date(self):
+ return self.talks.filter(start_date__isnull=False).exclude(duration=0, category__duration=0).order_by('start_date').all()
+
+ @property
+ def unscheduled_talks(self):
+ return self.talks.filter(Q(start_date__isnull=True) | Q(duration=0, category__duration=0)).all()
+
+
+class TalkCategory(models.Model): # type of talk (conf 30min, 1h, stand, …)
+
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+ name = models.CharField(max_length=64)
+ duration = models.PositiveIntegerField(default=0, verbose_name=_('Default duration (min)'))
+ color = RGBColorField(default='#ffffff', verbose_name=_("Color on program"))
+ label = models.CharField(max_length=64, verbose_name=_("Label on program"), blank=True, default="")
+ opening_date = models.DateTimeField(null=True, blank=True, default=None)
+ closing_date = models.DateTimeField(null=True, blank=True, default=None)
+
+ def is_open(self):
+ now = timezone.now()
+ if self.opening_date and now < self.opening_date:
+ return False
+ if self.closing_date and now > self.closing_date:
+ return False
+ return True
+
+ class Meta:
+ unique_together = ('site', 'name')
+ ordering = ('pk',)
+ verbose_name = "category"
+ verbose_name_plural = "categories"
+
+ def __str__(self):
+ return ugettext(self.name)
+
+ def get_absolute_url(self):
+ return reverse('talk-list') + '?category=%d' % self.pk
+
+
+#class Attendee(PonyConfModel):
+#
+# user = models.ForeignKey(User, null=True)
+# name = models.CharField(max_length=64, blank=True, default="")
+# email = models.EmailField(blank=True, default="")
+#
+# def get_name(self):
+# if self.user:
+# return str(self.user.profile)
+# else:
+# return self.name
+# get_name.short_description = _('Name')
+#
+# def get_email(self):
+# if self.user:
+# return self.user.email
+# else:
+# return self.email
+# get_email.short_description = _('Email')
+#
+# def __str__(self):
+# return self.get_name()
+
+
+#def talk_materials_destination(talk, filename):
+# return join(talk.site.name, talk.slug, filename)
+
+
+class TalkManager(models.Manager):
+ def get_queryset(self):
+ qs = super().get_queryset()
+ qs = qs.annotate(score=Coalesce(Avg('vote__vote'), 0))
+ return qs
+
+
+class Talk(PonyConfModel):
+
+ LICENCES = (
+ ('CC-Zero CC-BY', 'CC-Zero CC-BY'),
+ ('CC-BY-SA', 'CC-BY-SA'),
+ ('CC-BY-ND', 'CC-BY-ND'),
+ ('CC-BY-NC', 'CC-BY-NC'),
+ ('CC-BY-NC-SA','CC-BY-NC-SA'),
+ ('CC-BY-NC-ND', 'CC-BY-NC-ND'),
+ )
+
+ site = models.ForeignKey(Site, on_delete=models.CASCADE)
+
+ speakers = models.ManyToManyField(Participant, verbose_name=_('Speakers'))
+ title = models.CharField(max_length=128, verbose_name=_('Talk Title'))
+ slug = AutoSlugField(populate_from='title', unique=True)
+ #abstract = models.CharField(max_length=255, blank=True, verbose_name=_('Abstract'))
+ description = models.TextField(verbose_name=_('Description of your talk'))
+ track = models.ForeignKey(Track, blank=True, null=True, verbose_name=_('Track'))
+ notes = models.TextField(blank=True, verbose_name=_('Message to organizers'), help_text=_('If you have any constraint or if you have anything that may help you to select your talk, like a video or slides of your talk, please write it down here'))
+ category = models.ForeignKey(TalkCategory, verbose_name=_('Talk Category'))
+ videotaped = models.BooleanField(_("I'm ok to be recorded on video"), default=True)
+ video_licence = models.CharField(choices=LICENCES, default='CC-BY-SA', max_length=10, verbose_name=_("Video licence"))
+ sound = models.BooleanField(_("I need sound"), default=False)
+ accepted = models.NullBooleanField(default=None)
+ start_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Beginning date and time'))
+ duration = models.PositiveIntegerField(default=0, verbose_name=_('Duration (min)'))
+ room = models.ForeignKey(Room, blank=True, null=True, default=None)
+ plenary = models.BooleanField(default=False)
+ #materials = models.FileField(null=True, upload_to=talk_materials_destination, verbose_name=_('Materials'),
+ # help_text=_('You can use this field to share some materials related to your intervention.'))
+
+ token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
+
+ conversation = models.OneToOneField(MessageThread)
+
+
+ objects = TalkManager()
+
+
+ class Meta:
+ ordering = ('title',)
+
+ def __str__(self):
+ return self.title
+
+ def get_speakers_str(self):
+ speakers = list(map(str, self.speakers.all()))
+ if len(speakers) == 0:
+ return 'superman'
+ elif len(speakers) == 1:
+ return speakers[0]
+ else:
+ return ', '.join(speakers[:-1]) + ' & ' + str(speakers[-1])
+
+ @property
+ def estimated_duration(self):
+ return self.duration or self.category.duration
+
+ def get_absolute_url(self):
+ return reverse('talk-details', kwargs={'talk_id': self.token})
+
+ @property
+ def end_date(self):
+ if self.estimated_duration:
+ return self.start_date + timedelta(minutes=self.estimated_duration)
+ else:
+ return None
+
+ @property
+ def dtstart(self):
+ return self.start_date.strftime('%Y%m%dT%H%M%SZ')
+
+ @property
+ def dtend(self):
+ return self.end_date.strftime('%Y%m%dT%H%M%SZ')
+
+ #@property
+ #def materials_name(self):
+ # return basename(self.materials.name)
+
+ class Meta:
+ ordering = ('category__id', 'title',)
+
+
+class Vote(PonyConfModel):
+
+ talk = models.ForeignKey(Talk)
+ user = models.ForeignKey(User)
+ vote = models.IntegerField(validators=[MinValueValidator(-2), MaxValueValidator(2)], default=0)
+
+ class Meta:
+ unique_together = ('talk', 'user')
+
+ def __str__(self):
+ return "%+i by %s for %s" % (self.vote, self.user, self.talk)
+
+ def get_absolute_url(self):
+ return self.talk.get_absolute_url()
diff --git a/cfp/signals.py b/cfp/signals.py
new file mode 100644
index 0000000..1675b8f
--- /dev/null
+++ b/cfp/signals.py
@@ -0,0 +1,110 @@
+from django.db.models.signals import pre_save, post_save
+from django.dispatch import receiver
+from django.contrib.sites.models import Site
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+
+from ponyconf.decorators import disable_for_loaddata
+from mailing.models import MessageThread, Message
+from .models import Participant, Talk, Conference
+
+
+@receiver(post_save, sender=Site, dispatch_uid="Create Conference for Site")
+@disable_for_loaddata
+def create_conference(sender, instance, **kwargs):
+ conference, created = Conference.objects.get_or_create(site=instance)
+
+
+def create_conversation(sender, instance, **kwargs):
+ if not hasattr(instance, 'conversation'):
+ instance.conversation = MessageThread.objects.create()
+pre_save.connect(create_conversation, sender=Participant)
+pre_save.connect(create_conversation, sender=Talk)
+
+
+@receiver(pre_save, sender=Message, dispatch_uid="Set message author")
+def set_message_author(sender, instance, **kwargs):
+ message = instance
+ if message.author is None:
+ # Try users
+ try:
+ instance.author = User.objects.get(email=message.from_email)
+ except User.DoesNotExist:
+ pass
+ else:
+ return
+ # Try participants
+ try:
+ instance.author = Participant.objects.get(email=message.from_email)
+ except User.DoesNotExist:
+ pass
+ else:
+ return
+ # Try conferences
+ try:
+ instance.author = Conference.objects.get(contact_email=message.from_email)
+ except Conference.DoesNotExist:
+ pass
+ else:
+ return
+
+
+@receiver(post_save, sender=Message, dispatch_uid="Send message notifications")
+def send_message_notifications(sender, instance, **kwargs):
+ message = instance
+ thread = message.thread
+ first_message = thread.message_set.first()
+ if message == first_message:
+ reference = None
+ else:
+ reference = first_message.token
+ subject_prefix = 'Re: ' if reference else ''
+ if hasattr(thread, 'participant'):
+ conf = thread.participant.site.conference
+ elif hasattr(thread, 'talk'):
+ conf = thread.talk.site.conference
+ message_id = '<{id}@%s>' % conf.site.domain
+ if conf.reply_email:
+ reply_to = (conf.name, conf.reply_email)
+ else:
+ reply_to = None
+ sender = (message.author_display, conf.contact_email)
+ staff_dests = [ (user.get_full_name(), user.email) for user in conf.staff.all() ]
+ if hasattr(thread, 'participant'):
+ conf = thread.participant.site.conference
+ participant = thread.participant
+ participant_dests = [ (participant.name, participant.email) ]
+ participant_subject = _('[%(prefix)s] Message from the staff') % {'prefix': conf.name}
+ staff_subject = _('[%(prefix)s] Conversation with %(dest)s') % {'prefix': conf.name, 'dest': participant.name}
+ proto = 'https' if conf.secure_domain else 'http'
+ footer = '\n\n--\n%s://' % proto + conf.site.domain + reverse('participant-details', args=[participant.token])
+ if message.from_email == conf.contact_email: # this is a talk notification message
+ # send it only to the participant
+ message.send_notification(subject=subject_prefix+participant_subject, sender=sender, dests=participant_dests,
+ reply_to=reply_to, message_id=message_id, reference=reference)
+ else:
+ # this is a message between the staff and the participant
+ message.send_notification(subject=subject_prefix+staff_subject, sender=sender, dests=staff_dests,
+ reply_to=reply_to, message_id=message_id, reference=reference, footer=footer)
+ if message.from_email != thread.participant.email: # message from staff: sent it to the participant too
+ message.send_notification(subject=subject_prefix+participant_subject, sender=sender, dests=participant_dests,
+ reply_to=reply_to, message_id=message_id, reference=reference)
+ elif hasattr(thread, 'talk'):
+ conf = thread.talk.site.conference
+ subject = _('[%(prefix)s] Talk: %(talk)s') % {'prefix': conf.name, 'talk': thread.talk.title}
+ proto = 'https' if conf.secure_domain else 'http'
+ footer = '\n\n--\n%s://' % proto + conf.site.domain + reverse('talk-details', args=[thread.talk.token])
+ message.send_notification(subject=subject_prefix+subject, sender=sender, dests=staff_dests,
+ reply_to=reply_to, message_id=message_id, reference=reference, footer=footer)
+
+
+# connected in apps.py
+def call_first_site_post_save(apps, **kwargs):
+ try:
+ site = Site.objects.get(id=getattr(settings, 'SITE_ID', 1))
+ except Site.DoesNotExist:
+ pass
+ else:
+ site.save()
diff --git a/cfp/templates/cfp/closed.html b/cfp/templates/cfp/closed.html
new file mode 100644
index 0000000..792703c
--- /dev/null
+++ b/cfp/templates/cfp/closed.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+{% trans "Sorry, the Call for Participation is closed!" %}
+
+{% endblock %}
diff --git a/cfp/templates/cfp/complete.html b/cfp/templates/cfp/complete.html
new file mode 100644
index 0000000..c25b4a2
--- /dev/null
+++ b/cfp/templates/cfp/complete.html
@@ -0,0 +1,27 @@
+{% extends 'base.html' %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+
+
{% trans "Thanks for your proposal" %} {{ participant }} !
+
{% trans "You can at anytime:" %}
+
+
+
{% trans "An email has been sent to you with those URLs" %}
+
+
+{% endblock %}
diff --git a/proposals/templates/proposals/home.html b/cfp/templates/cfp/home.html
similarity index 63%
rename from proposals/templates/proposals/home.html
rename to cfp/templates/cfp/home.html
index b91fd71..412c415 100644
--- a/proposals/templates/proposals/home.html
+++ b/cfp/templates/cfp/home.html
@@ -1,11 +1,11 @@
{% extends 'base.html' %}
-{% load proposals_tags i18n %}
+{% load ponyconf_tags i18n %}
{% block hometab %} class="active"{% endblock %}
{% block content %}
-{% markdown site.conference.home %}
+{% markdown conference.home %}
{% endblock %}
diff --git a/cfp/templates/cfp/propose.html b/cfp/templates/cfp/propose.html
new file mode 100644
index 0000000..a59db31
--- /dev/null
+++ b/cfp/templates/cfp/propose.html
@@ -0,0 +1,27 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/cfp/templates/cfp/speaker.html b/cfp/templates/cfp/speaker.html
new file mode 100644
index 0000000..6a98b01
--- /dev/null
+++ b/cfp/templates/cfp/speaker.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% load ponyconf_tags i18n %}
+
+{% block proposetab %} class="active"{% endblock %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/ponyconf/templates/staff.html b/cfp/templates/cfp/staff/base.html
similarity index 57%
rename from ponyconf/templates/staff.html
rename to cfp/templates/cfp/staff/base.html
index 74713a5..fd50435 100644
--- a/ponyconf/templates/staff.html
+++ b/cfp/templates/cfp/staff/base.html
@@ -6,16 +6,24 @@
{% block navbar %}
{{ block.super }}
{% endblock %}
+
+{% block content %}
+{% trans "Please select a category." %}
+{% endblock %}
diff --git a/cfp/templates/cfp/staff/conference.html b/cfp/templates/cfp/staff/conference.html
new file mode 100644
index 0000000..852c44c
--- /dev/null
+++ b/cfp/templates/cfp/staff/conference.html
@@ -0,0 +1,27 @@
+{% extends 'cfp/staff/base.html' %}
+{% load i18n crispy_forms_tags %}
+
+{% block conferencetab %} class="active"{% endblock %}
+
+{% block content %}
+
+{% trans "Conference" %}
+
+
+
+{% endblock %}
+
+{% block js_end %}
+{{ block.super }}
+{{ form.media.js }}
+{% endblock %}
+
+{% block css %}
+{{ block.super }}
+{{ form.media.css }}
+{% endblock %}
diff --git a/cfp/templates/cfp/staff/create_user.html b/cfp/templates/cfp/staff/create_user.html
new file mode 100644
index 0000000..7e433cc
--- /dev/null
+++ b/cfp/templates/cfp/staff/create_user.html
@@ -0,0 +1,17 @@
+{% extends 'cfp/staff/base.html' %}
+{% load i18n crispy_forms_tags %}
+
+{% block conferencetab %} class="active"{% endblock %}
+
+{% block content %}
+
+{% trans "Add a new user" %}
+
+
+
+{% endblock %}
diff --git a/cfp/templates/cfp/staff/participant_details.html b/cfp/templates/cfp/staff/participant_details.html
new file mode 100644
index 0000000..0b55dcd
--- /dev/null
+++ b/cfp/templates/cfp/staff/participant_details.html
@@ -0,0 +1,62 @@
+{% extends 'cfp/staff/base.html' %}
+{% load i18n %}
+
+{% block speakerstab %} class="active"{% endblock %}
+
+{% block content %}
+
+{{ participant }}
+
+{% trans "Edit" %}
+
+{% trans "Biography" %}
+{{ participant.biography|linebreaksbr }}
+
+{% if participant.notes %}
+{% trans "Notes" %}
+{{ participant.notes|linebreaksbr }}
+{% endif %}
+
+{% trans "Informations" %}
+
+
+{% trans "Talks" %}
+{% regroup participant.talk_set.all by category as category_list %}
+{% for category in category_list %}
+{{ category.list.0.category }}
+{% for talk in category.list %}
+
+ {{ talk }}
+ {% trans "by" %}
+ {% for p in talk.speakers.all %}
+ {% if p == participant %}{{ p }}{% else %}{{ p }} {% endif %}
+ {% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
+ {% endfor %}
+ {% if talk.track %}
+ {% trans "in" %}
+ {{ talk.track }}
+ {% endif %}
+
+ {% endfor %}
+
+{% empty %}{% trans "No talks" %}
+{% endfor %}
+
+{% trans "Messaging" %}
+
+{% include 'mailing/_message_list.html' with messages=participant.conversation.message_set.all %}
+
+{% trans "Send a message – this message will be received by this participant and all the staff team " as message_form_title %}
+{% include 'mailing/_message_form.html' %}
+
+{% endblock %}
diff --git a/cfp/templates/cfp/staff/participant_form.html b/cfp/templates/cfp/staff/participant_form.html
new file mode 100644
index 0000000..997444b
--- /dev/null
+++ b/cfp/templates/cfp/staff/participant_form.html
@@ -0,0 +1,13 @@
+{% extends 'cfp/staff/base.html' %}
+{% load i18n crispy_forms_tags %}
+
+{% block speakerstab %} class="active"{% endblock %}
+
+{% block content %}
+
+{% trans "Edit a speaker" %}
+
+{% url 'participant-details' participant.token as cancel_url %}
+{% include '_form.html' %}
+
+{% endblock %}
diff --git a/proposals/templates/proposals/speaker_list.html b/cfp/templates/cfp/staff/participant_list.html
similarity index 56%
rename from proposals/templates/proposals/speaker_list.html
rename to cfp/templates/cfp/staff/participant_list.html
index 055d72c..ea90495 100644
--- a/proposals/templates/proposals/speaker_list.html
+++ b/cfp/templates/cfp/staff/participant_list.html
@@ -1,5 +1,4 @@
-{% extends 'staff.html' %}
-
+{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
{% block speakerstab %} class="active"{% endblock %}
@@ -8,6 +7,7 @@
{% trans "Speakers" %}
+{% comment %}
{% trans "Show filtering options…" %}
@@ -39,63 +39,43 @@
+{% endcomment %}
- {% trans "Total:" %} {{ speaker_list|length }} {% trans "speaker" %}{{ speaker_list|length|pluralize }}
+ {% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }}
- {% trans "Username" %}
- {% trans "Fullname" %}
+ {% trans "Name" %}
{% trans "Talk count" %}
- {% blocktrans context "table column title" %}Need transport?{% endblocktrans %}
- {% blocktrans context "table column title" %}Need accommodation?{% endblocktrans %}
- {% trans "Need sound?" %}
-
+ {% comment %} {% endcomment %}
+ {% comment %}
{% trans "Contact:" %} {% trans "link" %}
- {% for speaker in speaker_list %}
+ {% endcomment %}
+ {% for participant in participant_list %}
{% if forloop.first %}
{% endif %}
- {{ speaker.user.username }}
- {{ speaker.user.get_full_name }}
- {{ speaker.not_refused_talk_set.count }}{% if speaker.pending_talk_set.count %} ({{ speaker.pending_talk_set.count }} pending){% endif %}
- {% if speaker.need_transport %}
-
- {% for transport in speaker.transport.all %}
- {% if not forloop.first %}, {% endif %}
- {{ transport }}
- {% empty %}
- Yes
- {% endfor %}
+ {{ participant }}
+
+ {% blocktrans count accepted=participant.accepted_talk_count %}accepted: {{ accepted }}{% plural %}accepted: {{ accepted }}{% endblocktrans %}
+ —
+ {% blocktrans count pending=participant.pending_talk_count %}pending: {{ pending }}{% plural %}pending: {{ pending }}{% endblocktrans %}
+ —
+ {% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %}
- {% elif speaker.need_transport is None %}
- ?
- {% else %}
- No
- {% endif %}
-
- {% if speaker.accommodation is None %}
- ?
- {% else %}
- {{ speaker.get_accommodation_display }}
- {% endif %}
-
- {% if speaker.sound %}
- Yes
- {% else %}
- No
- {% endif %}
+ {% comment %}
{% trans "Contact" %}
+ {% endcomment %}
{% if forloop.last %}
@@ -105,6 +85,7 @@
{% endblock %}
+{% comment %}
{% block js_end %}
{% endblock %}
+{% endcomment %}
diff --git a/cfp/templates/cfp/staff/room_details.html b/cfp/templates/cfp/staff/room_details.html
new file mode 100644
index 0000000..5c309ab
--- /dev/null
+++ b/cfp/templates/cfp/staff/room_details.html
@@ -0,0 +1,48 @@
+{% extends 'cfp/staff/base.html' %}
+
+{% load bootstrap3 cfp_tags i18n %}
+
+{% block roomstab %} class="active"{% endblock %}
+
+{% block content %}
+
+{{ room.name }}
+ {{ room.label }}
+
+
+{% trans "Scheduled talks" %}
+{% for talk in room.talks_by_date %}
+{% if forloop.first %}{% endif %}
+
+ {{ talk }}
+ {% for participant in talk.speakers.all %}
+ {% if forloop.first %} – {% endif %}
+ {{ participant }}
+ {% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
+ {% if forloop.last %} {% endif %}
+ {% endfor %}
+ – {{ talk.start_date }} – {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}
+
+{% if forloop.last %} {% endif %}
+{% empty %}
+{% trans "No talks." %}
+{% endfor %}
+
+{% trans "Unscheduled talks" %}
+{% for talk in room.unscheduled_talks %}
+{% if forloop.first %}{% endif %}
+
+ {{ talk }}
+ {% for participant in talk.speakers.all %}
+ {% if forloop.first %} – {% endif %}
+ {{ participant }}
+ {% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
+ {% if forloop.last %} {% endif %}
+ {% endfor %}
+
+{% if forloop.last %} {% endif %}
+{% empty %}
+{% trans "No talks." %}
+{% endfor %}
+
+{% endblock %}
diff --git a/proposals/templates/proposals/topic_form.html b/cfp/templates/cfp/staff/room_form.html
similarity index 67%
rename from proposals/templates/proposals/topic_form.html
rename to cfp/templates/cfp/staff/room_form.html
index 5e50057..393955d 100644
--- a/proposals/templates/proposals/topic_form.html
+++ b/cfp/templates/cfp/staff/room_form.html
@@ -1,8 +1,8 @@
-{% extends 'staff.html' %}
+{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
-{% block topicstab %} class="active"{% endblock %}
+{% block roomstab %} class="active"{% endblock %}
{% block css %}
{{ block.super }}
@@ -11,7 +11,7 @@
{% block content %}
-{% trans "Topic" %}
+{% trans "Room" %}
{% include "_form.html" %}
diff --git a/cfp/templates/cfp/staff/room_list.html b/cfp/templates/cfp/staff/room_list.html
new file mode 100644
index 0000000..0016d1d
--- /dev/null
+++ b/cfp/templates/cfp/staff/room_list.html
@@ -0,0 +1,46 @@
+{% extends 'cfp/staff/base.html' %}
+
+{% load bootstrap3 cfp_tags i18n %}
+
+{% block roomstab %} class="active"{% endblock %}
+
+{% block content %}
+
+{% trans "Rooms" %}
+
+{% trans "Add a room" %}
+
+
+ {% for room in room_list %}
+
+
+ {% if room.label %}
{{ room.label }}
{% endif %}
+
+ {{ room.capacity }} {% trans "place" %}{{ room.capacity|pluralize }}
+ |
+
+ {{ room.talks.count }} {% trans "talk" %}{{ room.talks.count|pluralize }}
+
+ |
+ {% bootstrap_icon "pencil" %}
+
+
+ {% cycle '' '
' %}
+ {% cycle '' '' '
' %}
+ {% empty %}
+
{% trans "No rooms." %}
+ {% endfor %}
+
+
+{% endblock %}
+
+{% block js_end %}
+{{ block.super }}
+
+{% endblock %}
diff --git a/proposals/templates/proposals/talk_decide.html b/cfp/templates/cfp/staff/talk_decide.html
similarity index 77%
rename from proposals/templates/proposals/talk_decide.html
rename to cfp/templates/cfp/staff/talk_decide.html
index 5df73f6..6d4ca56 100644
--- a/proposals/templates/proposals/talk_decide.html
+++ b/cfp/templates/cfp/staff/talk_decide.html
@@ -1,8 +1,7 @@
-{% extends 'base.html' %}
-
+{% extends 'cfp/staff/base.html' %}
{% load i18n %}
-{% block listingtab %} active{% endblock %}
+{% block talkstab %} class="active"{% endblock %}
{% block content %}
@@ -10,17 +9,17 @@
{% trans "Information about the proposals" %}
{% trans "Title:" %} {{ talk.title }}
-{% trans "Kind:" %} {{ talk.event }}
+{% trans "Kind:" %} {{ talk.category }}
{% trans "Information for the proposer" %}
-
{% endblock %}
diff --git a/proposals/templates/proposals/talk_detail.html b/cfp/templates/cfp/staff/talk_details.html
similarity index 52%
rename from proposals/templates/proposals/talk_detail.html
rename to cfp/templates/cfp/staff/talk_details.html
index e95e19d..5d9c21d 100644
--- a/proposals/templates/proposals/talk_detail.html
+++ b/cfp/templates/cfp/staff/talk_details.html
@@ -1,50 +1,38 @@
-{% extends base_template %}
+{% extends 'cfp/staff/base.html' %}
+{% load bootstrap3 i18n %}
-{% if staff %}
{% block talkstab %} class="active"{% endblock %}
-{% else %}
-{% block exhibitortab %} class="active"{% endblock %}
-{% endif %}
-
-{% load i18n %}
{% block content %}
{{ talk.title }}
-{% if edit_perm %}
-{% trans "Edit" %}
-{% endif %}
+{% trans "Edit" %}
-{% if talk.abstract %}{{ talk.abstract }}{% else %}{% trans "No abstract provided." %} {% endif %}
-
-{% if moderate_perm %}
+{% trans "Information" %}
- {% trans "Format" %}
- {{ talk.event }}
- {% trans "Topics" %}
- {% for topic in talk.topics.all %}
- {{ topic }} {% if not forloop.last %}, {% endif %}
- {% empty %}
- {% trans "No topics." %}
- {% endfor %}
+ {% trans "Category" %}
+ {{ talk.category }}
+
+ {% trans "Status" %}
+ {{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}
{% trans "Track" %}
{% if talk.track %}
{{ talk.track }}
{% else %}
- {% trans "No assigned yet." %}
+ {% trans "No assigned yet." context "session" %}
{% endif %}
- Horaire
+ {% trans "Timeslot" %}
{% if talk.start_date %}
{{ talk.start_date|date:"l d b" }} ,
{{ talk.start_date|date:"H:i" }} – {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}
{% else %}{% trans "not defined" %}
{% endif %}
- Salle
+ {% trans "Room" %}
{% if talk.room %}
{{ talk.room }}
@@ -52,6 +40,7 @@
{% else %}{% trans "not defined" %}
{% endif %}
+ {% comment %}
{% if talk.registration_required %}
{% trans "Registrations" %}
{% if talk.attendees_limit %}{{ talk.attendees.count }} / {{ talk.attendees_limit }}{% else %}{% trans "required but unlimited" %}{% endif %}
@@ -64,69 +53,49 @@
{% trans "Video" %}
{% trans "download" %}
{% endif %}
+ {% endcomment %}
-{% endif %}
-
{% trans "Description" %}
{% if talk.description %}{{ talk.description|linebreaksbr }}{% else %}{% trans "No description provided." %} {% endif %}
{% trans "Speakers" %}
-{% for speaker in talk.speakers.all %}
+{% for participant in talk.speakers.all %}
{% if forloop.first %}{% endif %}
{% empty %}
{% trans "No speakers." %}
{% endfor %}
-{% if moderate_perm %}
-
-{% if not talk.track %}
-{% trans "Track" %}
-{% trans "No assigned yet." %}
- {% for topic in talk.topics.distinct %}
- {% if forloop.first %}{% endif %}
- {% if topic.track %}
- {% trans "Assign to" %} {{ topic.track }}
- {% endif %}
- {% if forloop.last %}
{% endif %}
- {% endfor %}
-{% endif %}
-
-{% endif %}
-
{% trans "Notes" %}
{% if talk.notes %}{{ talk.notes|linebreaksbr }}{% else %}{% trans "No notes." %} {% endif %}
-{% if moderate_perm %}
-
{% trans "Moderation" %}
{% trans "Status" %}
-{{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}
-
{% if talk.accepted == None %}
{% trans "Vote" %}
-{{ talk.vote_set.all|length }} {% trans "vote" %}{{ talk.vote_set.all|length|pluralize }}, {% trans "average:" %} {{ talk.score|floatformat:1 }}
+{{ talk.vote_set.count }} {% trans "vote" %}{{ talk.vote_set.count|pluralize }}, {% trans "average:" %} {{ talk.score|floatformat:1 }}
-Accept
-Decline
+Accept
+Decline
{% endif %}
+{% comment %}
{% if talk.registration_required %}
{% trans "Attendees" %}
@@ -139,15 +108,13 @@
{% endfor %}
{% endif %}
+{% endcomment %}
-{% trans "Messages" %}
-{% trans "These messages are for organization team only." %}
-{% for message in talk.conversation.messages.all %}
-{% include 'conversations/_message_detail.html' %}
-{% endfor %}
+{% trans "Messaging" %}
-{% include 'conversations/_message_form.html' %}
+{% include 'mailing/_message_list.html' with messages=talk.conversation.message_set.all %}
-{% endif %}
+{% trans "Comment this talk – this message will be received by the staff team only " as message_form_title %}
+{% include 'mailing/_message_form.html' %}
{% endblock %}
diff --git a/cfp/templates/cfp/staff/talk_form.html b/cfp/templates/cfp/staff/talk_form.html
new file mode 100644
index 0000000..6123e62
--- /dev/null
+++ b/cfp/templates/cfp/staff/talk_form.html
@@ -0,0 +1,13 @@
+{% extends 'cfp/staff/base.html' %}
+{% load i18n crispy_forms_tags %}
+
+{% block talkstab %} class="active"{% endblock %}
+
+{% block content %}
+
+{% trans "Edit a talk" %}
+
+{% url 'talk-details' talk.token as cancel_url %}
+{% include '_form.html' %}
+
+{% endblock %}
diff --git a/proposals/templates/proposals/talk_list.html b/cfp/templates/cfp/staff/talk_list.html
similarity index 57%
rename from proposals/templates/proposals/talk_list.html
rename to cfp/templates/cfp/staff/talk_list.html
index 4a12d8f..6503f4c 100644
--- a/proposals/templates/proposals/talk_list.html
+++ b/cfp/templates/cfp/staff/talk_list.html
@@ -1,6 +1,5 @@
-{% extends 'staff.html' %}
-
-{% load bootstrap3 i18n accounts_tags %}
+{% extends 'cfp/staff/base.html' %}
+{% load bootstrap3 i18n %}
{% block talkstab %} class="active"{% endblock %}
@@ -16,19 +15,14 @@
-
+
{% bootstrap_field filter_form.status layout="horizontal" %}
- {% bootstrap_field filter_form.kind layout="horizontal" %}
+ {% bootstrap_field filter_form.category layout="horizontal" %}
{% bootstrap_field filter_form.vote layout="horizontal" %}
- {% bootstrap_field filter_form.room layout="horizontal" %}
{% bootstrap_field filter_form.scheduled layout="horizontal" %}
- {% bootstrap_field filter_form.materials layout="horizontal" %}
- {% bootstrap_field filter_form.video layout="horizontal" %}
+ {% bootstrap_field filter_form.room layout="horizontal" %}
-
- {% bootstrap_field filter_form.topic layout="horizontal" %}
-
-
+
{% bootstrap_field filter_form.track layout="horizontal" %}
@@ -37,17 +31,14 @@
-
-
-{% if action_form %}
-
-
-
{% trans "For selected talks:" %}
- {% csrf_token %}
- {% bootstrap_field action_form.decision %}
- {% if request|orga %}
- {% bootstrap_field action_form.track %}
- {% bootstrap_field action_form.room %}
- {% endif %}
- {% buttons %}
- {% trans "Apply" %}
- {% endbuttons %}
-
-
-{% endif %}
-
-
-
-{% endblock %}
-
-{% block js_end %}
-
{% endblock %}
diff --git a/proposals/templates/proposals/track_form.html b/cfp/templates/cfp/staff/track_form.html
similarity index 89%
rename from proposals/templates/proposals/track_form.html
rename to cfp/templates/cfp/staff/track_form.html
index 8fc12db..2f463c3 100644
--- a/proposals/templates/proposals/track_form.html
+++ b/cfp/templates/cfp/staff/track_form.html
@@ -1,4 +1,4 @@
-{% extends 'staff.html' %}
+{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
diff --git a/proposals/templates/proposals/track_list.html b/cfp/templates/cfp/staff/track_list.html
similarity index 78%
rename from proposals/templates/proposals/track_list.html
rename to cfp/templates/cfp/staff/track_list.html
index 924cc19..67f065b 100644
--- a/proposals/templates/proposals/track_list.html
+++ b/cfp/templates/cfp/staff/track_list.html
@@ -1,6 +1,6 @@
-{% extends 'staff.html' %}
+{% extends 'cfp/staff/base.html' %}
-{% load bootstrap3 accounts_tags proposals_tags i18n %}
+{% load bootstrap3 cfp_tags i18n %}
{% block trackstab %} class="active"{% endblock %}
@@ -8,9 +8,7 @@
{% trans "Tracks" %}
-{% if request|orga %}
-
{% trans "Add a track" %}
-{% endif %}
+
{% trans "Add a track" %}
{% cycle '' '
' %}
diff --git a/accounts/templatetags/__init__.py b/cfp/templatetags/__init__.py
similarity index 100%
rename from accounts/templatetags/__init__.py
rename to cfp/templatetags/__init__.py
diff --git a/proposals/templatetags/proposals_tags.py b/cfp/templatetags/cfp_tags.py
similarity index 66%
rename from proposals/templatetags/proposals_tags.py
rename to cfp/templatetags/cfp_tags.py
index 2656151..297676d 100644
--- a/proposals/templatetags/proposals_tags.py
+++ b/cfp/templatetags/cfp_tags.py
@@ -1,14 +1,14 @@
from django import template
-from proposals.utils import markdown_to_html
+from cfp.utils import is_staff
register = template.Library()
-@register.simple_tag
-def markdown(value):
- return markdown_to_html(value)
+@register.filter
+def staff(request):
+ return is_staff(request, request.user)
@register.filter('duration_format')
def duration_format(value):
diff --git a/cfp/urls.py b/cfp/urls.py
new file mode 100644
index 0000000..fb0cd70
--- /dev/null
+++ b/cfp/urls.py
@@ -0,0 +1,54 @@
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+ url(r'^$', views.home, name='home'),
+ url(r'^cfp/$', views.talk_proposal, name='talk-proposal'),
+ url(r'^cfp/(?P
[\w\-]+)/speaker/add/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-add'),
+ url(r'^cfp/(?P[\w\-]+)/speaker/(?P[\w\-]+)/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-edit'),
+ url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/$', views.talk_proposal, name='talk-proposal-edit'),
+ url(r'^staff/$', views.staff, name='staff'),
+ url(r'^staff/conference/$', views.conference, name='conference'),
+ url(r'^staff/talks/$', views.talk_list, name='talk-list'),
+ url(r'^staff/talks/(?P[\w\-]+)/$', views.talk_details, name='talk-details'),
+ url(r'^staff/talks/(?P[\w\-]+)/vote/(?P[-+0-2]+)/$', views.talk_vote, name='talk-vote'),
+ url(r'^staff/talks/(?P[\w\-]+)/accept/$', views.talk_decide, {'accept': True}, name='talk-accept'),
+ url(r'^staff/talks/(?P[\w\-]+)/decline/$', views.talk_decide, {'accept': False}, name='talk-decline'),
+ url(r'^staff/talks/(?P[\w\-]+)/edit/$', views.TalkUpdate.as_view(), name='talk-edit'),
+ url(r'^staff/speakers/$', views.participant_list, name='participant-list'),
+ url(r'^staff/speakers/(?P[\w\-]+)/$', views.participant_details, name='participant-details'),
+ url(r'^staff/speakers/(?P[\w\-]+)/edit/$', views.ParticipantUpdate.as_view(), name='participant-edit'),
+ url(r'^staff/tracks/$', views.TrackList.as_view(), name='track-list'),
+ url(r'^staff/tracks/add/$', views.TrackCreate.as_view(), name='track-add'),
+ url(r'^staff/tracks/(?P[-\w]+)/edit/$', views.TrackUpdate.as_view(), name='track-edit'),
+ url(r'^staff/rooms/$', views.RoomList.as_view(), name='room-list'),
+ url(r'^staff/rooms/add/$', views.RoomCreate.as_view(), name='room-add'),
+ url(r'^staff/rooms/(?P[-\w]+)/$', views.RoomDetail.as_view(), name='room-details'),
+ url(r'^staff/rooms/(?P[-\w]+)/edit/$', views.RoomUpdate.as_view(), name='room-edit'),
+ url(r'^staff/add-user/$', views.create_user, name='create-user'),
+ url(r'^staff/select2/$', views.Select2View.as_view(), name='django_select2-json'),
+
+ #url(r'^markdown/$', views.markdown_preview, name='markdown'),
+ #url(r'^$', views.home, name='home'),
+ #url(r'^staff/$', views.staff, name='staff'),
+ #url(r'^conference/$', views.conference, name='edit-conference'),
+ #url(r'^talk/propose/$', views.participate, name='participate-as-speaker'),
+ #url(r'^talk/$', views.talk_list, name='list-talks'),
+ #url(r'^talk/add/$', views.talk_edit, name='add-talk'),
+ #url(r'^talk/edit/(?P[-\w]+)$', views.talk_edit, name='edit-talk'),
+ #url(r'^talk/vote/(?P[-\w]+)/(?P[-0-2]+)$', views.vote, name='vote'),
+ #url(r'^talk/details/(?P[-\w]+)$', views.TalkDetail.as_view(), name='show-talk'),
+ #url(r'^talk/accept/(?P[-\w]+)/$', views.talk_decide, {'accepted': True}, name='accept-talk'),
+ #url(r'^talk/decline/(?P[-\w]+)/$', views.talk_decide, {'accepted': False}, name='decline-talk'),
+ #url(r'^talk/assign-to-track/(?P[-\w]+)/(?P[-\w]+)/$', views.talk_assign_to_track, name='assign-talk-to-track'),
+ #url(r'^topic/$', views.TopicList.as_view(), name='list-topics'),
+ #url(r'^topic/add/$', views.TopicCreate.as_view(), name='add-topic'),
+ #url(r'^topic/(?P[-\w]+)/edit/$', views.TopicUpdate.as_view(), name='edit-topic'),
+ #url(r'^track/$', views.TrackList.as_view(), name='list-tracks'),
+ #url(r'^track/add/$', views.TrackCreate.as_view(), name='add-track'),
+ #url(r'^track/(?P[-\w]+)/edit/$', views.TrackUpdate.as_view(), name='edit-track'),
+ #url(r'^speakers/$', views.speaker_list, name='list-speakers'),
+ #url(r'^register/$', views.talk_registrable_list, name='list-registrable-talks'),
+ #url(r'^register/(?P[-\w]+)$', views.talk_register, name='register-for-a-talk'),
+]
diff --git a/proposals/utils.py b/cfp/utils.py
similarity index 62%
rename from proposals/utils.py
rename to cfp/utils.py
index a6a44c5..2404de9 100644
--- a/proposals/utils.py
+++ b/cfp/utils.py
@@ -1,10 +1,8 @@
-from django.contrib.sites.shortcuts import get_current_site
+from django.utils.crypto import get_random_string
from django.db.models import Q, Sum
from django.db.models.functions import Coalesce
from django.utils.safestring import mark_safe
-from accounts.models import Participation
-
from markdown import markdown
import bleach
@@ -13,13 +11,22 @@ def query_sum(queryset, field):
return queryset.aggregate(s=Coalesce(Sum(field), 0))['s']
+def generate_user_uid():
+ return get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')
+
+
def allowed_talks(talks, request):
- if not Participation.objects.get(site=get_current_site(request), user=request.user).is_orga():
+ if not Participation.objects.get(site=request.conference.site, user=request.user).is_orga():
talks = talks.filter(Q(topics__reviewers=request.user) | Q(speakers=request.user) | Q(proposer=request.user))
return talks.distinct()
+
def markdown_to_html(md):
html = markdown(md)
allowed_tags = bleach.ALLOWED_TAGS + ['p', 'pre', 'span' ] + ['h%d' % i for i in range(1, 7) ]
html = bleach.clean(html, tags=allowed_tags)
return mark_safe(html)
+
+
+def is_staff(request, user):
+ return user.is_authenticated and (user.is_superuser or request.conference.staff.filter(pk=user.pk).exists())
diff --git a/cfp/views.py b/cfp/views.py
new file mode 100644
index 0000000..486a82a
--- /dev/null
+++ b/cfp/views.py
@@ -0,0 +1,438 @@
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse_lazy
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.views.generic import FormView, TemplateView
+from django.contrib import messages
+from django.db.models import Q
+from django.views.generic import CreateView, DetailView, ListView, UpdateView
+
+from django_select2.views import AutoResponseView
+
+from functools import reduce
+
+from mailing.models import Message
+from mailing.forms import MessageForm
+from .decorators import staff_required
+from .mixins import StaffRequiredMixin, OnSiteMixin
+from .utils import is_staff
+from .models import Participant, Talk, TalkCategory, Vote, Track, Room
+from .forms import TalkForm, TalkStaffForm, TalkFilterForm, ParticipantForm, ParticipantStaffForm, ConferenceForm, CreateUserForm, STATUS_VALUES, TrackForm, RoomForm
+
+
+def home(request):
+ if request.conference.home:
+ return render(request, 'cfp/home.html')
+ else:
+ return redirect(reverse('talk-proposal'))
+
+
+def talk_proposal(request, talk_id=None, participant_id=None):
+
+ conference = request.conference
+ site = conference.site
+ if is_staff(request, request.user):
+ categories = TalkCategory.objects.filter(site=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():
+ 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.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])
+ body = _("""Hi {},
+
+Your talk has been submitted for {}.
+
+Here are the details of your talk:
+Title: {}
+Description: {}
+
+You can at anytime:
+- edit your talk: {}
+- add a new co-speaker: {}
+- edit your profile: {}
+
+If you have any question, your can answer to this email.
+
+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)
+
+ Message.objects.create(
+ thread=participant.conversation,
+ author=conference,
+ from_email=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,
+ 'talk_form': talk_form,
+ })
+
+
+def talk_proposal_speaker_edit(request, talk_id, participant_id=None):
+
+ talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
+ participant = None
+
+ if participant_id:
+ participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
+
+ participant_form = ParticipantForm(request.POST or None, instance=participant)
+
+ if request.method == 'POST' and participant_form.is_valid():
+
+ 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()
+
+ talk.speakers.add(participant)
+
+ return render(request,'cfp/complete.html', {'talk': talk, 'participant': participant})
+
+ return render(request, 'cfp/speaker.html', {
+ 'participant_form': participant_form,
+ })
+
+
+@staff_required
+def staff(request):
+ return render(request, 'cfp/staff/base.html')
+
+
+@staff_required
+def talk_list(request):
+ show_filters = False
+ talks = Talk.objects.filter(site=request.conference.site)
+ filter_form = TalkFilterForm(request.GET or None, site=request.conference.site)
+ # Filtering
+ if filter_form.is_valid():
+ data = filter_form.cleaned_data
+ if len(data['category']):
+ show_filters = True
+ talks = talks.filter(reduce(lambda x, y: x | y, [Q(category__pk=pk) for pk in data['category']]))
+ if len(data['status']):
+ show_filters = True
+ talks = talks.filter(reduce(lambda x, y: x | y, [Q(accepted=dict(STATUS_VALUES)[status]) for status in data['status']]))
+ if data['room'] != None:
+ show_filters = True
+ talks = talks.filter(room__isnull=not data['room'])
+ if data['scheduled'] != None:
+ show_filters = True
+ talks = talks.filter(start_date__isnull=not data['scheduled'])
+ if len(data['track']):
+ show_filters = True
+ q = Q()
+ if 'none' in data['track']:
+ data['track'].remove('none')
+ q |= Q(track__isnull=True)
+ if len(data['track']):
+ q |= Q(track__slug__in=data['track'])
+ talks = talks.filter(q)
+ if data['vote'] != None:
+ show_filters = True
+ if data['vote']:
+ talks = talks.filter(vote__user=request.user)
+ else:
+ talks = talks.exclude(vote__user=request.user)
+ # Sorting
+ if request.GET.get('order') == 'desc':
+ sort_reverse = True
+ else:
+ sort_reverse = False
+ SORT_MAPPING = {
+ 'title': 'title',
+ 'category': 'category',
+ 'status': 'accepted',
+ }
+ sort = request.GET.get('sort')
+ if sort in SORT_MAPPING.keys():
+ if sort_reverse:
+ talks = talks.order_by('-' + SORT_MAPPING[sort])
+ else:
+ talks = talks.order_by(SORT_MAPPING[sort])
+ # Sorting URLs
+ sort_urls = dict()
+ sort_glyphicons = dict()
+ for c in SORT_MAPPING.keys():
+ url = request.GET.copy()
+ url['sort'] = c
+ if c == sort:
+ if sort_reverse:
+ del url['order']
+ glyphicon = 'sort-by-attributes-alt'
+ else:
+ url['order'] = 'desc'
+ glyphicon = 'sort-by-attributes'
+ else:
+ glyphicon = 'sort'
+ sort_urls[c] = url.urlencode()
+ sort_glyphicons[c] = glyphicon
+ talks = talks.prefetch_related('category', 'speakers', 'track')
+ return render(request, 'cfp/staff/talk_list.html', {
+ 'show_filters': show_filters,
+ 'talk_list': talks,
+ 'filter_form': filter_form,
+ 'sort_urls': sort_urls,
+ 'sort_glyphicons': sort_glyphicons,
+ })
+
+
+@staff_required
+def talk_details(request, talk_id):
+ talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
+ message_form = MessageForm(request.POST or None)
+ if request.method == 'POST' and message_form.is_valid():
+ message = message_form.save(commit=False)
+ message.author = request.user
+ message.from_email = request.user.email
+ message.thread = talk.conversation
+ message.save()
+ messages.success(request, _('Message sent!'))
+ return redirect(reverse('talk-details', args=[talk.token]))
+ return render(request, 'cfp/staff/talk_details.html', {
+ 'talk': talk,
+ })
+
+
+@staff_required
+def talk_vote(request, talk_id, score):
+ talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
+ vote, created = Vote.objects.get_or_create(talk=talk, user=request.user)
+ vote.vote = int(score)
+ vote.save()
+ messages.success(request, _('Vote successfully created') if created else _('Vote successfully updated'))
+ return redirect(talk.get_absolute_url())
+
+
+@staff_required
+def talk_decide(request, talk_id, accept):
+ talk = get_object_or_404(Talk, token=talk_id, site=request.conference.site)
+ if request.method == 'POST':
+ # Does we need to send a notification to the proposer?
+ m = request.POST.get('message', '').strip()
+ if m:
+ for participant in talk.speakers.all():
+ Message.objects.create(thread=talk.conversation, author=request.user, content=m)
+ # Save the decision in the talk's conversation
+ if accept:
+ note = _("The talk has been accepted.")
+ else:
+ note = _("The talk has been declined.")
+ Message.objects.create(thread=talk.conversation, author=request.user, content=note)
+ talk.accepted = accept
+ talk.save()
+ messages.success(request, _('Decision taken in account'))
+ return redirect(talk.get_absolute_url())
+ return render(request, 'cfp/staff/talk_decide.html', {
+ 'talk': talk,
+ 'accept': accept,
+ })
+
+
+@staff_required
+def participant_list(request):
+ participants = Participant.objects.filter(site=request.conference.site) \
+ .extra(select={'lower_name': 'lower(name)'}) \
+ .order_by('lower_name')
+ return render(request, 'cfp/staff/participant_list.html', {
+ 'participant_list': participants,
+ })
+
+
+@staff_required
+def participant_details(request, participant_id):
+ participant = get_object_or_404(Participant, token=participant_id, site=request.conference.site)
+ message_form = MessageForm(request.POST or None)
+ if request.method == 'POST' and message_form.is_valid():
+ message = message_form.save(commit=False)
+ message.author = request.user
+ message.from_email = request.user.email
+ message.thread = participant.conversation
+ message.save()
+ messages.success(request, _('Message sent!'))
+ return redirect(reverse('participant-details', args=[participant.token]))
+ return render(request, 'cfp/staff/participant_details.html', {
+ 'participant': participant,
+ })
+
+
+class ParticipantUpdate(StaffRequiredMixin, OnSiteMixin, UpdateView):
+ model = Participant
+ slug_field = 'token'
+ slug_url_kwarg = 'participant_id'
+ form_class = ParticipantStaffForm
+ template_name = 'cfp/staff/participant_form.html'
+
+
+@staff_required
+def conference(request):
+ form = ConferenceForm(request.POST or None, instance=request.conference)
+
+ if request.method == 'POST' and form.is_valid():
+ old_staff = set(request.conference.staff.all())
+ new_conference = form.save()
+ new_staff = set(new_conference.staff.all())
+ added_staff = new_staff - old_staff
+ protocol = 'https' if request.is_secure() else 'http'
+ base_url = protocol+'://'+request.conference.site.domain
+ url_login = base_url + reverse('login')
+ url_password_reset = base_url + reverse('password_reset')
+ msg_title = _('[{}] You have been added to the staff team').format(request.conference.name)
+ msg_body_template = _("""Hi {},
+
+You have been added to the staff team.
+
+You can now:
+- login: {}
+- reset your password: {}
+
+{}
+
+""")
+ # TODO: send bulk emails
+ for user in added_staff:
+ msg_body = msg_body_template.format(user.get_full_name(), url_login, url_password_reset, request.conference.name)
+ send_mail(
+ msg_title,
+ msg_body,
+ request.conference.from_email(),
+ [user.email],
+ fail_silently=False,
+ )
+ messages.success(request, _('Modifications successfully saved.'))
+ return redirect(reverse('conference'))
+
+ return render(request, 'cfp/staff/conference.html', {
+ 'form': form,
+ })
+
+
+class TalkUpdate(StaffRequiredMixin, OnSiteMixin, UpdateView):
+ model = Talk
+ slug_field = 'token'
+ slug_url_kwarg = 'talk_id'
+ form_class = TalkStaffForm
+ template_name = 'cfp/staff/talk_form.html'
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs.update({
+ 'categories': TalkCategory.objects.filter(site=self.request.conference.site),
+ 'tracks': Track.objects.filter(site=self.request.conference.site),
+ })
+ return kwargs
+
+
+class TrackMixin(OnSiteMixin):
+ model = Track
+
+
+class TrackList(StaffRequiredMixin, TrackMixin, ListView):
+ template_name = 'cfp/staff/track_list.html'
+
+
+class TrackFormMixin(TrackMixin):
+ template_name = 'cfp/staff/track_form.html'
+ form_class = TrackForm
+ success_url = reverse_lazy('track-list')
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs.update({
+ 'conference': self.request.conference,
+ })
+ return kwargs
+
+
+class TrackCreate(StaffRequiredMixin, TrackFormMixin, CreateView):
+ pass
+
+
+class TrackUpdate(StaffRequiredMixin, TrackFormMixin, UpdateView):
+ pass
+
+
+class RoomMixin(OnSiteMixin):
+ model = Room
+
+
+class RoomList(StaffRequiredMixin, RoomMixin, ListView):
+ template_name = 'cfp/staff/room_list.html'
+
+
+class RoomDetail(StaffRequiredMixin, RoomMixin, DetailView):
+ template_name = 'cfp/staff/room_details.html'
+
+
+class RoomFormMixin(RoomMixin):
+ template_name = 'cfp/staff/room_form.html'
+ form_class = RoomForm
+ success_url = reverse_lazy('room-list')
+
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs.update({
+ 'conference': self.request.conference,
+ })
+ return kwargs
+
+
+class RoomCreate(StaffRequiredMixin, RoomFormMixin, CreateView):
+ pass
+
+
+class RoomUpdate(StaffRequiredMixin, RoomFormMixin, UpdateView):
+ pass
+
+
+@staff_required
+def create_user(request):
+ form = CreateUserForm(request.POST or None)
+
+ if request.method == 'POST' and form.is_valid():
+ form.save()
+ messages.success(request, _('User created successfully.'))
+ return redirect(reverse('create-user'))
+
+ return render(request, 'cfp/staff/create_user.html', {
+ 'form': form,
+ })
+
+
+class Select2View(StaffRequiredMixin, AutoResponseView):
+ pass
diff --git a/conversations/__init__.py b/conversations/__init__.py
deleted file mode 100644
index 113bdeb..0000000
--- a/conversations/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-default_app_config = 'conversations.apps.ConversationsConfig'
diff --git a/conversations/admin.py b/conversations/admin.py
deleted file mode 100644
index 431bad1..0000000
--- a/conversations/admin.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from django.contrib import admin
-
-from .models import ConversationAboutTalk, ConversationWithParticipant, Message
-
-admin.site.register(ConversationWithParticipant)
-admin.site.register(ConversationAboutTalk)
-admin.site.register(Message)
diff --git a/conversations/apps.py b/conversations/apps.py
deleted file mode 100644
index 7fc0e2b..0000000
--- a/conversations/apps.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from django.apps import AppConfig
-
-
-class ConversationsConfig(AppConfig):
- name = 'conversations'
-
- def ready(self):
- import conversations.signals # noqa
diff --git a/conversations/emails.py b/conversations/emails.py
deleted file mode 100644
index ba00c6a..0000000
--- a/conversations/emails.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import re
-import chardet
-import logging
-from functools import reduce
-
-from email import policy
-from email.parser import BytesParser
-from email.message import EmailMessage
-
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.core.exceptions import PermissionDenied
-from django.http import Http404, HttpResponse
-from django.shortcuts import get_object_or_404
-from django.views.decorators.csrf import csrf_exempt
-from django.views.decorators.http import require_http_methods
-
-from .models import Message
-from .utils import hexdigest_sha256
-
-
-@csrf_exempt
-@require_http_methods(["POST"])
-def email_recv(request):
-
- if not hasattr(settings, 'REPLY_EMAIL') \
- or not hasattr(settings, 'REPLY_KEY'):
- return HttpResponse(status=501) # Not Implemented
-
- key = request.POST.get('key').strip()
- if key != settings.REPLY_KEY:
- raise PermissionDenied
-
- if 'email' not in request.FILES:
- return HttpResponse(status=400) # Bad Request
-
- msg = request.FILES['email']
-
- msg = BytesParser(policy=policy.default).parsebytes(msg.read())
- body = msg.get_body(preferencelist=('plain',))
- content = body.get_payload(decode=True)
-
- try:
- content = content.decode(body.get_content_charset())
- except Exception:
- encoding = chardet.detect(content)['encoding']
- content = content.decode(encoding)
-
- addr = settings.REPLY_EMAIL
- pos = addr.find('@')
- name = addr[:pos]
- domain = addr[pos+1:]
-
- regexp = '^%s\+(?P[a-z0-9]{12})(?P[a-z0-9]{60})(?P[a-z0-9]{12})@%s$' % (name, domain)
- p = re.compile(regexp)
- m = None
- addrs = map(lambda x: x.split(',') if x else [], [msg.get('To'), msg.get('Cc')])
- addrs = reduce(lambda x, y: x + y, addrs)
- for _mto in map(lambda x: x.strip(), addrs):
- m = p.match(_mto)
- if m:
- break
- if not m: # no one matches
- raise Http404
-
- author = get_object_or_404(User, profile__email_token=m.group('dest'))
- message = get_object_or_404(Message, token=m.group('token'))
- key = hexdigest_sha256(settings.SECRET_KEY, message.token, author.pk)[0:12]
- if key != m.group('key'):
- raise PermissionDenied
-
- answer = Message(conversation=message.conversation,
- author=author, content=content)
- answer.save()
-
- return HttpResponse()
diff --git a/conversations/migrations/0001_initial.py b/conversations/migrations/0001_initial.py
deleted file mode 100644
index 2784369..0000000
--- a/conversations/migrations/0001_initial.py
+++ /dev/null
@@ -1,64 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-01-13 10:49
-from __future__ import unicode_literals
-
-import conversations.utils
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('contenttypes', '0002_remove_content_type_name'),
- ('accounts', '0001_initial'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='ConversationAboutTalk',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('subscribers', models.ManyToManyField(blank=True, related_name='_conversationabouttalk_subscribers_+', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.CreateModel(
- name='ConversationWithParticipant',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Participation')),
- ('subscribers', models.ManyToManyField(blank=True, related_name='_conversationwithparticipant_subscribers_+', to=settings.AUTH_USER_MODEL)),
- ],
- options={
- 'abstract': False,
- },
- ),
- migrations.CreateModel(
- name='Message',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('updated', models.DateTimeField(auto_now=True)),
- ('object_id', models.PositiveIntegerField()),
- ('token', models.CharField(default=conversations.utils.generate_message_token, max_length=64, unique=True)),
- ('content', models.TextField(blank=True)),
- ('system', models.BooleanField(default=False)),
- ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
- ],
- options={
- 'ordering': ['created'],
- },
- ),
- ]
diff --git a/conversations/migrations/0002_conversationabouttalk_talk.py b/conversations/migrations/0002_conversationabouttalk_talk.py
deleted file mode 100644
index ddaa634..0000000
--- a/conversations/migrations/0002_conversationabouttalk_talk.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.3 on 2017-01-13 10:49
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('proposals', '0001_initial'),
- ('conversations', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='conversationabouttalk',
- name='talk',
- field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='proposals.Talk'),
- ),
- ]
diff --git a/conversations/models.py b/conversations/models.py
deleted file mode 100644
index f05b7ef..0000000
--- a/conversations/models.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from django.contrib.auth.models import User
-from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
-from django.contrib.contenttypes.models import ContentType
-from django.core.urlresolvers import reverse
-from django.db import models
-
-from accounts.models import Participation
-from ponyconf.utils import PonyConfModel
-from proposals.models import Talk
-
-from .utils import generate_message_token, notify_by_email
-
-
-class Message(PonyConfModel):
-
- content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
- object_id = models.PositiveIntegerField()
- conversation = GenericForeignKey('content_type', 'object_id')
-
- token = models.CharField(max_length=64, default=generate_message_token, unique=True)
-
- author = models.ForeignKey(User)
- content = models.TextField(blank=True)
- system = models.BooleanField(default=False)
-
- class Meta:
- ordering = ['created']
-
- def __str__(self):
- return "Message from %s" % self.author
-
- def get_absolute_url(self):
- return self.conversation.get_absolute_url()
-
-
-class Conversation(PonyConfModel):
-
- subscribers = models.ManyToManyField(User, related_name='+', blank=True)
-
- class Meta:
- abstract = True
-
-
-class ConversationWithParticipant(Conversation):
-
- participation = models.OneToOneField(Participation, related_name='conversation')
- messages = GenericRelation(Message)
-
- uri = 'inbox'
- template = 'participant_message'
-
- def __str__(self):
- return "Conversation with %s" % self.participation.user
-
- def get_absolute_url(self):
- return reverse('user-conversation', kwargs={'username': self.participation.user.username})
-
- def get_site(self):
- return self.participation.site
-
- def new_message(self, message):
- site = self.get_site()
- subject = '[%s] Conversation with %s' % (site.name, self.participation.user.profile)
- recipients = list(self.subscribers.all())
- # Auto-subscribe
- if message.author != self.participation.user and message.author not in recipients:
- self.subscribers.add(message.author)
- data = {
- 'content': message.content,
- 'uri': site.domain + reverse('user-conversation', args=[self.participation.user.username]),
- }
- first = self.messages.first()
- if first != message:
- ref = first.token
- else:
- ref = None
- notify_by_email('message', data, subject, message.author, recipients, message.token, ref)
-
- if message.author != self.participation.user:
- subject = '[%s] Message notification' % site.name
- data.update({
- 'uri': site.domain + reverse('inbox')
- })
- notify_by_email('message', data, subject, message.author, [self.participation.user], message.token, ref)
-
-
-class ConversationAboutTalk(Conversation):
-
- talk = models.OneToOneField(Talk, related_name='conversation')
- messages = GenericRelation(Message)
-
- uri = 'inbox'
- template = 'talk_message'
-
- def __str__(self):
- return "Conversation about %s" % self.talk.title
-
- def get_absolute_url(self):
- return self.talk.get_absolute_url()
-
- def get_site(self):
- return self.talk.site
-
- def new_message(self, message):
- site = self.get_site()
- first = self.messages.first()
- if not message.system and message.author not in self.subscribers.all():
- self.subscribers.add(message.author)
- recipients = self.subscribers.all()
- data = {
- 'uri': site.domain + reverse('show-talk', args=[self.talk.slug]),
- }
- if first == message:
- subject = '[%s] Talk: %s' % (site.name, self.talk.title)
- template = 'talk_notification'
- ref = None
- data.update({
- 'talk': self.talk,
- 'proposer': message.author,
- 'proposer_uri': site.domain + reverse('show-participant', args=[message.author.username])
- })
- else:
- subject = 'Re: [%s] Talk: %s' % (site.name, self.talk.title)
- template = 'message'
- ref = first.token
- data.update({'content': message.content})
- notify_by_email(template, data, subject, message.author, recipients, message.token, ref)
diff --git a/conversations/post-mail.sh b/conversations/post-mail.sh
deleted file mode 100755
index 3b04715..0000000
--- a/conversations/post-mail.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#! /bin/bash
-
-# Usage: cat email.txt | post-mail.sh REPLY_KEY@https://example.org/conversations/recv/
-# Get the value of REPLY_KEY from the django setting.
-
-# Postfix users can set up an alias file with this content:
-# reply: "|/path/to/post-mail.sh mykey@https://example.org/conversations/recv/
-# don't forget to run postalias and to add the alias file to main.cf under alias_map.
-
-curl ${@#*\@} -F key=${@%\@*} -F "email=@-;filename=email.txt"
diff --git a/conversations/sieve-filter b/conversations/sieve-filter
deleted file mode 100755
index c64b574..0000000
--- a/conversations/sieve-filter
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-
-import requests
-
-if len(sys.argv) != 2:
- print("Usage: %s KEY@URL" % sys.argv[0])
- sys.exit(1)
-
-key, url = sys.argv[1].split('@')
-
-email = sys.stdin.buffer.raw.read()
-sys.stdout.buffer.write(email) # DO NOT REMOVE
-
-requests.post(
- url,
- data={
- 'key': key,
- },
- files={
- 'email': ('email.txt', email),
- }
-)
diff --git a/conversations/signals.py b/conversations/signals.py
deleted file mode 100644
index e3c7d4c..0000000
--- a/conversations/signals.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from django.contrib.auth.models import User
-from django.db.models import Q
-from django.db.models.signals import post_save
-from django.dispatch import receiver
-
-from ponyconf.decorators import disable_for_loaddata
-from accounts.models import Participation
-from proposals.models import Talk
-from proposals.signals import talk_added, talk_edited
-
-from .models import ConversationAboutTalk, ConversationWithParticipant, Message
-
-
-@receiver(post_save, sender=Participation, dispatch_uid="Create ConversationWithParticipant")
-@disable_for_loaddata
-def create_conversation_with_participant(sender, instance, created, **kwargs):
- if not created:
- return
- conversation = ConversationWithParticipant(participation=instance)
- conversation.save()
-
-
-@receiver(post_save, sender=Talk, dispatch_uid="Create ConversationAboutTalk")
-@disable_for_loaddata
-def create_conversation_about_talk(sender, instance, created, **kwargs):
- if not created:
- return
- conversation = ConversationAboutTalk(talk=instance)
- conversation.save()
-
-
-def check_talk(talk):
- reviewers = User.objects.filter(Q(topic__talk=talk) | Q(participation__site=talk.site, participation__orga=True))
- # Subscribe the reviewers to the conversation about the talk
- talk.conversation.subscribers.add(*reviewers)
- # Subscribe the reviewers to the conversations with each speaker
- for user in talk.speakers.all():
- participation, created = Participation.objects.get_or_create(user=user, site=talk.site)
- participation.conversation.subscribers.add(*reviewers)
-
-
-@receiver(talk_added, dispatch_uid="Notify talk added")
-def notify_talk_added(sender, instance, author, **kwargs):
- check_talk(instance)
- message = Message(conversation=instance.conversation, author=author,
- content='The talk has been proposed.', system=True)
- message.save()
-
-
-@receiver(talk_edited, dispatch_uid="Notify talk edited")
-def notify_talk_edited(sender, instance, author, **kwargs):
- check_talk(instance)
- message = Message(conversation=instance.conversation, author=author,
- content='The talk has been modified.', system=True)
- message.save()
-
-
-@receiver(post_save, sender=Message, dispatch_uid="Notify new message")
-@disable_for_loaddata
-def notify_new_message(sender, instance, created, **kwargs):
- if not created:
- # Possibly send a modification notification?
- return
- instance.conversation.new_message(instance)
diff --git a/conversations/templates/conversations/_message_detail.html b/conversations/templates/conversations/_message_detail.html
deleted file mode 100644
index 3766a26..0000000
--- a/conversations/templates/conversations/_message_detail.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
- {{ message.content|linebreaksbr }}
-
-
diff --git a/conversations/templates/conversations/conversation.html b/conversations/templates/conversations/conversation.html
deleted file mode 100644
index 47438f6..0000000
--- a/conversations/templates/conversations/conversation.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends 'staff.html' %}
-
-{% load i18n %}
-
-{% block correspondentstab %} class="active"{% endblock %}
-
-{% block content %}
-
-{% blocktrans with correspondent=correspondent.profile %}Conversation with {{ correspondent }}{% endblocktrans %}
-
-{% for message in message_list %}
-{% include 'conversations/_message_detail.html' %}
-{% endfor %}
-
-{% include 'conversations/_message_form.html' %}
-
-{% endblock %}
diff --git a/conversations/templates/conversations/correspondent_list.html b/conversations/templates/conversations/correspondent_list.html
deleted file mode 100644
index d4590fa..0000000
--- a/conversations/templates/conversations/correspondent_list.html
+++ /dev/null
@@ -1,36 +0,0 @@
-{% extends 'staff.html' %}
-
-{% load bootstrap3 i18n %}
-
-{% block correspondentstab %} class="active"{% endblock %}
-
-{% block content %}
-
-{% trans "Correspondents" %}
-{% trans "This is the list of participants that you follow." %}
-
-
-
- #
- Username
- Full name
- Administration
-
- {% for correspondent in correspondent_list %}
-
- {{ forloop.counter }}
- {{ correspondent.user.username }}
- {{ correspondent.user.get_full_name }}
-
-
- {% if request.user in correspondent.conversation.subscribers.all %}
-
- {% else %}
-
- {% endif %}
-
-
- {% endfor %}
-
-
-{% endblock %}
diff --git a/conversations/templates/conversations/emails/message.html b/conversations/templates/conversations/emails/message.html
deleted file mode 100644
index af1a03d..0000000
--- a/conversations/templates/conversations/emails/message.html
+++ /dev/null
@@ -1,8 +0,0 @@
-{{ content|linebreaksbr }}
-
-
-{% if answering %}
-Reply to this email directly or view it online .
-{% else %}
-Reply online .
-{% endif %}
diff --git a/conversations/templates/conversations/emails/message.txt b/conversations/templates/conversations/emails/message.txt
deleted file mode 100644
index 060506a..0000000
--- a/conversations/templates/conversations/emails/message.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-{{ content|safe }}
-
---
-Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }}
diff --git a/conversations/templates/conversations/emails/talk_notification.html b/conversations/templates/conversations/emails/talk_notification.html
deleted file mode 100644
index 1db0dae..0000000
--- a/conversations/templates/conversations/emails/talk_notification.html
+++ /dev/null
@@ -1,11 +0,0 @@
-Hi!
-
-A new {{ talk.event }} has been proposed by {{ proposer.profile }} !
-
-Title: {{ talk.title }}
-
-Description:
-{{ talk.description|linebreaksbr }}
-{% if answering %}
-
-Reply to this email directly to comment this talk.{% endif %}
diff --git a/conversations/templates/conversations/emails/talk_notification.txt b/conversations/templates/conversations/emails/talk_notification.txt
deleted file mode 100644
index e02d1cc..0000000
--- a/conversations/templates/conversations/emails/talk_notification.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-Hi!
-
-A new talk has been proposed by {{ proposer.profile }}!
-See it online: https://{{ uri }}
-
-Title: {{ talk.title }}
-
-Description:
-{{ talk.description }}
-{% if answering %}
---
-Reply to this email directly to comment this talk.{% endif %}
diff --git a/conversations/templates/conversations/inbox.html b/conversations/templates/conversations/inbox.html
deleted file mode 100644
index 4fc83df..0000000
--- a/conversations/templates/conversations/inbox.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends 'base.html' %}
-
-{% load i18n %}
-
-{% block inboxtab %} class="active"{% endblock %}
-
-{% block content %}
-
-{% trans "Messaging" %}
-{% trans "You can use this page to communicate with the staff." %}
-
-{% for message in message_list %}
-{% include 'conversations/_message_detail.html' %}
-{% endfor %}
-
-{% include 'conversations/_message_form.html' %}
-
-{% endblock %}
diff --git a/conversations/tests.py b/conversations/tests.py
deleted file mode 100644
index 54acbce..0000000
--- a/conversations/tests.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from django.contrib.auth.models import User
-from django.contrib.sites.models import Site
-from django.core.urlresolvers import reverse
-from django.test import TestCase, override_settings
-from django.core import mail
-from django.conf import settings
-
-from accounts.models import Participation
-from proposals.models import Topic, Talk, Event
-
-from .models import ConversationAboutTalk, ConversationWithParticipant, Message
-
-
-class ConversationTests(TestCase):
- def setUp(self):
- a, b, c, d = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abcd')
- d.is_superuser = True
- d.save()
- pa, _ = Participation.objects.get_or_create(user=a, site=Site.objects.first())
- conversation, _ = ConversationWithParticipant.objects.get_or_create(participation=pa)
- Message.objects.create(content='allo', conversation=conversation, author=b)
- Message.objects.create(content='aluil', conversation=conversation, author=a)
- site = Site.objects.first()
- Talk.objects.get_or_create(site=site, proposer=a, title='a talk', description='yay', event=Event.objects.get(site=site, name='other'))
-
- def test_models(self):
- talk, participant, message = (model.objects.first() for model in
- (ConversationAboutTalk, ConversationWithParticipant, Message))
- self.assertEqual(str(talk), 'Conversation about a talk')
- self.assertEqual(str(participant), 'Conversation with a')
- self.assertEqual(str(message), 'Message from b')
- self.assertEqual(message.get_absolute_url(), '/conversations/with/a/')
- self.assertEqual(talk.get_absolute_url(), '/talk/details/a-talk')
-
- def test_views(self):
- url = ConversationWithParticipant.objects.first().get_absolute_url()
- self.assertEqual(self.client.get(url).status_code, 302)
- self.client.login(username='c', password='c')
- self.assertEqual(self.client.get(url).status_code, 403)
- self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 403) # c is not staff
- self.assertEqual(self.client.get(reverse('inbox')).status_code, 200)
- self.client.post(reverse('inbox'), {'content': 'coucou'})
- self.client.login(username='d', password='d')
- self.client.post(url, {'content': 'im superuser'})
- self.assertEqual(Message.objects.last().content, 'im superuser')
- self.client.login(username='d', password='d')
- self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 200)
-
-
-@override_settings(DEFAULT_FROM_EMAIL='noreply@example.org',
- REPLY_EMAIL='reply@example.org',
- REPLY_KEY='secret')
-class EmailTests(TestCase):
- def setUp(self):
- for guy in 'abcd':
- setattr(self, guy, User.objects.create_user(guy, email='%s@example.org' % guy, password=guy))
- a_p = Participation(user=self.a, site=Site.objects.first())
- a_p.orga = True
- a_p.save()
- t = Topic(name='Topic 1', site=Site.objects.first())
- t.save()
- t.reviewers.add(self.b)
-
-
- def test_talk_notification(self):
- self.client.login(username='c', password='c')
- # Check that login create participation
- self.assertTrue(Participation.objects.filter(user=self.c, site=Site.objects.first()).exists())
- # Propose new talk
- topic = Topic.objects.get(name='Topic 1')
- response = self.client.post(reverse('add-talk'), {
- 'title': 'Talk 1',
- 'description': 'This is the first talk',
- 'topics': (topic.pk,),
- 'event': 1,
- 'speakers': (self.c.pk, self.d.pk),
- }, follow=True)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Talk proposed') # check messages notification
- talk = Talk.objects.get(site=Site.objects.first(), title='Talk 1')
- conv = ConversationAboutTalk.objects.get(talk=talk)
- # Orga and reviewer should have been subscribed to the conversation about the talk
- self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all()))
- # Both should have received an email notification
- self.assertEqual(len(mail.outbox), 2)
- for m in mail.outbox:
- self.assertEqual(m.from_email, '%s <%s>' % (self.c.profile, settings.DEFAULT_FROM_EMAIL))
- self.assertTrue('Talk: %s' % talk.title)
- self.assertTrue(len(m.to), 1)
- self.assertTrue(m.to[0] in [ self.a.email, self.b.email ])
- # Both should have been subscribed to conversations with each speakers
- for user in [self.c, self.d]:
- # Participation should have been created as the user is a speaker
- p = Participation.objects.get(user=user, site=Site.objects.first())
- conv = ConversationWithParticipant.objects.get(participation=p)
- self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all()))
diff --git a/conversations/urls.py b/conversations/urls.py
deleted file mode 100644
index 34fd082..0000000
--- a/conversations/urls.py
+++ /dev/null
@@ -1,13 +0,0 @@
-from django.conf.urls import url
-
-from conversations import emails, views
-
-urlpatterns = [
- url(r'^recv/$', emails.email_recv), # API
- url(r'^inbox/$', views.user_conversation, name='inbox'),
- url(r'^$', views.correspondent_list, name='list-correspondents'),
- url(r'^with/(?P[\w.@+-]+)/$', views.user_conversation, name='user-conversation'),
- url(r'^about/(?P[\w.@+-]+)/$', views.talk_conversation, name='talk-conversation'),
- url(r'^subscribe/(?P[\w.@+-]+)/$', views.subscribe, name='subscribe-conversation'),
- url(r'^unsubscribe/(?P[\w.@+-]+)/$', views.unsubscribe, name='unsubscribe-conversation'),
-]
diff --git a/conversations/utils.py b/conversations/utils.py
deleted file mode 100644
index 1a48cda..0000000
--- a/conversations/utils.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import hashlib
-
-from django.conf import settings
-from django.core import mail
-from django.core.mail import EmailMultiAlternatives
-from django.template.loader import render_to_string
-from django.utils.crypto import get_random_string
-
-
-def hexdigest_sha256(*args):
-
- r = hashlib.sha256()
- for arg in args:
- r.update(str(arg).encode('utf-8'))
-
- return r.hexdigest()
-
-
-def get_reply_addr(message_id, dest):
-
- if not hasattr(settings, 'REPLY_EMAIL'):
- return []
-
- addr = settings.REPLY_EMAIL
- pos = addr.find('@')
- name = addr[:pos]
- domain = addr[pos:]
- key = hexdigest_sha256(settings.SECRET_KEY, message_id, dest.pk)[0:12]
-
- return ['%s+%s%s%s%s' % (name, dest.profile.email_token, message_id, key, domain)]
-
-
-def generate_message_token():
- return get_random_string(length=60, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')
-
-
-def notify_by_email(template, data, subject, sender, dests, message_id, ref=None):
-
- if hasattr(settings, 'REPLY_EMAIL') and hasattr(settings, 'REPLY_KEY'):
- data.update({'answering': True})
-
- text_message = render_to_string('conversations/emails/%s.txt' % template, data)
- html_message = render_to_string('conversations/emails/%s.html' % template, data)
-
- from_email = '{name} <{email}>'.format(
- name=sender.get_full_name() or sender.username,
- email=settings.DEFAULT_FROM_EMAIL)
-
- # Generating headers
- headers = {'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL)}
- if ref:
- # This email reference a previous one
- headers.update({
- 'References': '<%s.%s>' % (ref, settings.DEFAULT_FROM_EMAIL),
- })
-
- mails = []
- for dest in dests:
- if not dest.email:
- continue
-
- reply_to = get_reply_addr(message_id, dest)
-
- mails += [(subject, (text_message, html_message), from_email, [dest.email], reply_to, headers)]
-
- messages = []
- for subject, message, from_email, dest_emails, reply_to, headers in mails:
- text_message, html_message = message
- msg = EmailMultiAlternatives(subject, text_message, from_email, dest_emails, reply_to=reply_to,
- headers=headers)
- msg.attach_alternative(html_message, 'text/html')
- messages += [msg]
- with mail.get_connection() as connection:
- connection.send_messages(messages)
diff --git a/conversations/views.py b/conversations/views.py
deleted file mode 100644
index 476876c..0000000
--- a/conversations/views.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.models import User
-from django.contrib.sites.shortcuts import get_current_site
-from django.core.exceptions import PermissionDenied
-from django.core.urlresolvers import reverse
-from django.shortcuts import get_object_or_404, redirect, render
-from django.utils.translation import ugettext_lazy as _
-
-from accounts.decorators import staff_required
-from accounts.models import Participation
-from proposals.models import Talk
-
-from .forms import MessageForm
-
-
-@login_required
-def user_conversation(request, username=None):
-
- if username:
- p = Participation.objects.get(user=request.user, site=get_current_site(request))
- if not p.is_staff() and not p.is_orga():
- raise PermissionDenied()
- user = get_object_or_404(User, username=username)
- template = 'conversations/conversation.html'
- else:
- user = request.user
- template = 'conversations/inbox.html'
-
- participation = get_object_or_404(Participation, user=user, site=get_current_site(request))
- conversation = participation.conversation
- message_list = conversation.messages.all()
-
- form = MessageForm(request.POST or None)
-
- if request.method == 'POST' and form.is_valid():
- form.instance.conversation = conversation
- form.instance.author = request.user
- form.save()
- messages.success(request, _('Message sent!'))
- if username:
- return redirect(reverse('user-conversation', args=[username]))
- else:
- return redirect('inbox')
-
- return render(request, template, {
- 'correspondent': user,
- 'message_list': message_list,
- 'form': form,
- })
-
-
-@login_required
-def talk_conversation(request, talk):
-
- talk = get_object_or_404(Talk, slug=talk)
- form = MessageForm(request.POST or None)
-
- if request.method == 'POST' and form.is_valid():
- form.instance.conversation = talk.conversation
- form.instance.author = request.user
- form.save()
- messages.success(request, 'Message sent!')
-
- return redirect(talk.get_absolute_url())
-
-
-@staff_required
-def correspondent_list(request):
-
- correspondent_list = Participation.objects.filter(site=get_current_site(request),
- conversation__subscribers=request.user)
-
- return render(request, 'conversations/correspondent_list.html', {
- 'correspondent_list': correspondent_list,
- })
-
-
-@staff_required
-def subscribe(request, username):
-
- participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request))
- participation.conversation.subscribers.add(request.user)
- messages.success(request, 'Subscribed.')
-
- next_url = request.GET.get('next') or reverse('user-conversation', args=[username])
-
- return redirect(next_url)
-
-
-@staff_required
-def unsubscribe(request, username):
-
- participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request))
- participation.conversation.subscribers.remove(request.user)
- messages.success(request, 'Unsubscribed.')
-
- next_url = request.GET.get('next') or reverse('user-conversation', args=[username])
-
- return redirect(next_url)
diff --git a/locale/fr/LC_MESSAGES/django.mo b/locale/fr/LC_MESSAGES/django.mo
index 10fa80a..b25001e 100644
Binary files a/locale/fr/LC_MESSAGES/django.mo and b/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index 561c8f7..2db855c 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -3,12 +3,11 @@
# This file is distributed under the same license as the PonyConf package.
# FIRST AUTHOR , YEAR.
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2016-11-25 13:33+0000\n"
+"POT-Creation-Date: 2017-08-12 00:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -25,81 +24,33 @@ msgstr ""
"Par exemple, vous devez rentrer le samedi soir, vous ne pouvez pas manger de "
"viande, etc."
-#: accounts/models.py:18
+#: accounts/models.py:18 cfp/models.py:96
msgid "Phone number"
msgstr "Numéro de téléphone"
-#: accounts/models.py:19
-#: accounts/templates/accounts/participant_details.html:20
-msgid "Biography"
-msgstr "Biographie"
+#: accounts/models.py:20 cfp/models.py:89
+msgid "Twitter"
+msgstr ""
-#: accounts/models.py:54
-msgid "No"
-msgstr "Non"
+#: accounts/models.py:21 cfp/models.py:90
+msgid "LinkedIn"
+msgstr ""
-#: accounts/models.py:55
-msgid "Hotel"
-msgstr "Hotel"
+#: accounts/models.py:22 cfp/models.py:91
+msgid "Github"
+msgstr ""
-#: accounts/models.py:56
-msgid "Homestay"
-msgstr "Logement chez habitant"
+#: accounts/models.py:23 cfp/models.py:92
+msgid "Website"
+msgstr ""
-#: accounts/models.py:63
-msgid "Defray transportation?"
-msgstr "Défraiement du transport ?"
+#: accounts/models.py:24 cfp/models.py:93
+msgid "Facebook"
+msgstr ""
-#: accounts/models.py:66
-msgid "I want to travel by"
-msgstr "Je souhaite voyager en"
-
-#: accounts/models.py:67
-msgid "Departure city"
-msgstr "Ville de départ"
-
-#: accounts/models.py:68
-msgid "Return city"
-msgstr "Ville de retour"
-
-#: accounts/models.py:68
-msgid "If different from departure city"
-msgstr "Si différent de la ville de départ"
-
-#: accounts/models.py:71
-msgid "Need accommodation?"
-msgstr "Besoin d'un hébergement ?"
-
-#: accounts/models.py:74
-#: accounts/templates/accounts/participant_details.html:68
-msgid "Constraints"
-msgstr "Contraintes"
-
-#: accounts/models.py:75
-msgid "I can output"
-msgstr "Sortie vidéo"
-
-#: accounts/models.py:76
-msgid "I need sound"
-msgstr "J’ai besoin de son"
-
-#: accounts/models.py:78
-msgid "I'm ok to be recorded on video"
-msgstr "J’accepte d’être enregistré en vidéo"
-
-#: accounts/models.py:79
-msgid "Video licence"
-msgstr "Licence vidéo"
-
-#: accounts/models.py:81
-#: accounts/templates/accounts/participant_details.html:71
-#: proposals/models.py:161 proposals/templates/proposals/talk_detail.html:98
-msgid "Notes"
-msgstr "Notes"
-
-#: accounts/models.py:81
-msgid "This field is only visible by organizers."
-msgstr "Ce champs est uniquement visible par les organisateurs."
+#: accounts/models.py:25 cfp/models.py:94
+msgid "Mastodon"
+msgstr ""
#: accounts/signals.py:16
msgid "Train"
@@ -144,17 +95,27 @@ msgid "Contact"
msgstr "Contacter"
#: accounts/templates/accounts/participant_details.html:15
+#: cfp/templates/cfp/staff/participant_details.html:10
+#: cfp/templates/cfp/staff/talk_details.html:10
#: proposals/templates/proposals/talk_detail.html:16
msgid "Edit"
msgstr "Éditer"
+#: accounts/templates/accounts/participant_details.html:20 cfp/models.py:86
+#: cfp/templates/cfp/staff/participant_details.html:12
+msgid "Biography"
+msgstr "Biographie"
+
#: accounts/templates/accounts/participant_details.html:27
-#: ponyconf/templates/staff.html:11
+#: cfp/templates/cfp/staff/base.html:17
+#: cfp/templates/cfp/staff/participant_details.html:33
+#: cfp/templates/cfp/staff/talk_list.html:8
#: proposals/templates/proposals/talk_list.html:9
msgid "Talks"
msgstr "Exposés"
#: accounts/templates/accounts/participant_details.html:32
+#: cfp/templates/cfp/staff/talk_details.html:12
msgid "Information"
msgstr "Informations"
@@ -210,13 +171,23 @@ msgstr "Accepte d’être enregistré en vidéo :"
msgid "Video licence:"
msgstr "Licence vidéo :"
+#: accounts/templates/accounts/participant_details.html:68
+msgid "Constraints"
+msgstr "Contraintes"
+
+#: accounts/templates/accounts/participant_details.html:71 cfp/forms.py:53
+#: cfp/models.py:100 cfp/templates/cfp/staff/participant_details.html:16
+#: cfp/templates/cfp/staff/talk_details.html:75 proposals/models.py:161
+#: proposals/templates/proposals/talk_detail.html:102
+msgid "Notes"
+msgstr "Notes"
+
#: accounts/templates/accounts/participant_edit.html:11
#, python-format
msgid "%(profile)s's profile"
msgstr "Profil de %(profile)s"
-#: accounts/templates/accounts/participant_list.html:9
-#: ponyconf/templates/staff.html:16 volunteers/models.py:15
+#: accounts/templates/accounts/participant_list.html:9 volunteers/models.py:15
msgid "Participants"
msgstr "Participants"
@@ -225,16 +196,14 @@ msgid "View conversation"
msgstr "Afficher la discussion"
#: accounts/templates/accounts/participant_list.html:31
-#: conversations/templates/conversations/correspondent_list.html:27
msgid "Unsubscribe from the conversation"
msgstr "Se désabonner de la discussion"
#: accounts/templates/accounts/participant_list.html:35
-#: conversations/templates/conversations/correspondent_list.html:29
msgid "Subscribe to the conversation"
msgstr "S’abonner à la discussion"
-#: accounts/templates/accounts/profile.html:12 ponyconf/templates/base.html:33
+#: accounts/templates/accounts/profile.html:12
msgid "Profile"
msgstr "Profil"
@@ -246,7 +215,9 @@ msgstr "Changer d’avatar"
msgid "Submit"
msgstr "Envoyer"
-#: accounts/templates/accounts/profile.html:36 ponyconf/templates/_form.html:16
+#: accounts/templates/accounts/profile.html:36
+#: cfp/templates/cfp/staff/create_user.html:14
+#: cfp/templates/cfp/staff/talk_decide.html:22 ponyconf/templates/_form.html:16
#: proposals/templates/proposals/talk_decide.html:23
msgid "Cancel"
msgstr "Annuler"
@@ -292,35 +263,6 @@ msgstr ""
msgid "Delete These"
msgstr "Supprimer ces"
-#: accounts/templates/registration/login.html:11
-#: ponyconf/templates/base.html:37
-msgid "Login"
-msgstr "Se connecter"
-
-#: accounts/templates/registration/login.html:26
-#, python-format
-msgid ""
-"You do not have an account yet? Please register ."
-msgstr "Pas encore de compte ? Enregistrez-vous ."
-
-#: accounts/templates/registration/password_change_form.html:9
-msgid "Password Change"
-msgstr "Changement de mot de passe"
-
-#: accounts/templates/registration/password_reset_form.html:9
-msgid "Password Reset"
-msgstr "Réinitialisation du mot de passe"
-
-#: accounts/templates/registration/registration_form.html:11
-msgid "Registration"
-msgstr "Inscription"
-
-#: accounts/templates/registration/registration_form.html:26
-#, python-format
-msgid ""
-"You already have an account? Please login ."
-msgstr "Déjà inscrit ? Connectez-vous ."
-
#: accounts/views.py:18
msgid "Reset your password"
msgstr "Réinitialiser son mot de passe"
@@ -347,156 +289,804 @@ msgstr "%(name)s a été ajouté aux participants"
msgid "%(name)s is already a participant"
msgstr "%(name)s est déjà participant"
-#: conversations/templates/conversations/_message_form.html:4
-msgid "Send a message"
-msgstr "Envoyer un message"
+#: cfp/forms.py:16
+msgid "Pending decision"
+msgstr "Décision en attente"
-#: conversations/templates/conversations/_message_form.html:12
-msgid "Send"
+#: cfp/forms.py:17 cfp/templates/cfp/staff/talk_list.html:66
+#: proposals/templates/proposals/talk_list.html:78
+msgid "Accepted"
+msgstr "Accepté"
+
+#: cfp/forms.py:18 cfp/templates/cfp/staff/talk_list.html:68
+#: proposals/templates/proposals/talk_list.html:80
+msgid "Declined"
+msgstr "Décliné"
+
+#: cfp/forms.py:50 cfp/forms.py:62 cfp/templates/cfp/staff/talk_details.html:15
+msgid "Category"
+msgstr "Catégorie"
+
+#: cfp/forms.py:51 cfp/templates/cfp/staff/talk_list.html:40
+#: proposals/models.py:155 proposals/templates/proposals/talk_list.html:47
+msgid "Title"
+msgstr "Titre"
+
+#: cfp/forms.py:52 cfp/models.py:135
+#: cfp/templates/cfp/staff/talk_details.html:61 proposals/models.py:54
+#: proposals/models.py:77 proposals/models.py:158
+#: proposals/templates/proposals/talk_detail.html:72 volunteers/models.py:14
+msgid "Description"
+msgstr "Description"
+
+#: cfp/forms.py:56
+msgid "Visible by speakers"
+msgstr "Visible par les orateurs"
+
+#: cfp/forms.py:68 cfp/templates/cfp/staff/talk_details.html:18
+#: cfp/templates/cfp/staff/talk_details.html:81
+#: cfp/templates/cfp/staff/talk_list.html:44
+#: proposals/templates/proposals/talk_detail.html:110
+#: proposals/templates/proposals/talk_list.html:52
+msgid "Status"
+msgstr "Statut"
+
+#: cfp/forms.py:74 cfp/models.py:267
+#: cfp/templates/cfp/staff/talk_details.html:21
+#: cfp/templates/cfp/staff/talk_list.html:43
+#: cfp/templates/cfp/staff/track_form.html:14 proposals/models.py:160
+#: proposals/templates/proposals/talk_detail.html:33
+#: proposals/templates/proposals/talk_detail.html:89
+#: proposals/templates/proposals/talk_list.html:51
+#: proposals/templates/proposals/track_form.html:14
+msgid "Track"
+msgstr "Session"
+
+#: cfp/forms.py:80 cfp/templates/cfp/staff/talk_details.html:84
+#: proposals/templates/proposals/talk_detail.html:115
+msgid "Vote"
+msgstr "Vote"
+
+#: cfp/forms.py:81 proposals/forms.py:72
+msgid "Filter talks you already / not yet voted for"
+msgstr ""
+"Filtrer les propositions pour lesquelles vous avez déjà voté / pas encore "
+"voté"
+
+#: cfp/forms.py:84 cfp/templates/cfp/staff/room_form.html:14
+#: cfp/templates/cfp/staff/talk_details.html:35
+msgid "Room"
+msgstr "Salle"
+
+#: cfp/forms.py:85 proposals/forms.py:73
+msgid "Filter talks already / not yet affected to a room"
+msgstr "Filtrer les exposés déjà / pas encore affectées à une salle"
+
+#: cfp/forms.py:88
+msgid "Scheduled"
+msgstr "Programmé"
+
+#: cfp/forms.py:89 proposals/forms.py:74
+msgid "Filter talks already / not yet scheduled"
+msgstr "Filtrer les exposés déjà / pas encore planifiées"
+
+#: cfp/forms.py:98
+msgid "Not assigned"
+msgstr "Pas encore assignée."
+
+#: cfp/forms.py:107 cfp/models.py:133
+#: cfp/templates/cfp/staff/participant_list.html:49 proposals/models.py:52
+#: proposals/models.py:75 proposals/models.py:132 volunteers/models.py:12
+msgid "Name"
+msgstr "Nom"
+
+#: cfp/forms.py:124
+msgid "New staff members will be informed of their new position by e-mail."
+msgstr ""
+"Les nouveaux membres du staff seront informés de leur nouveau rôle par "
+"courrier électronique."
+
+#: cfp/forms.py:144
+msgid "An user with that firstname and that lastname already exists."
+msgstr "Un utilisateur avec ce prénom et ce nom existe déjà."
+
+#: cfp/forms.py:149
+msgid "A user with that email already exists."
+msgstr "Un utilisateur avec cet email existe déjà."
+
+#: cfp/models.py:26
+msgid "Conference name"
+msgstr "Nom de la conférence"
+
+#: cfp/models.py:27
+msgid "Homepage (markdown)"
+msgstr "Page d’accueil (markdown)"
+
+#: cfp/models.py:28
+msgid "Venue information"
+msgstr "Informations sur le lieu"
+
+#: cfp/models.py:29
+msgid "City"
+msgstr "Ville"
+
+#: cfp/models.py:30
+msgid "Contact email"
+msgstr "Email de contact"
+
+#: cfp/models.py:31
+msgid "Reply email"
+msgstr "Adresse de réponse"
+
+#: cfp/models.py:32
+msgid "Staff members"
+msgstr "Membres du staff"
+
+#: cfp/models.py:33
+msgid "Secure domain (HTTPS)"
+msgstr "Domaine sécurisé (HTTPS)"
+
+#: cfp/models.py:61
+#, python-brace-format
+msgid ""
+"The reply email should be a formatable string accepting a token argument (e."
+"g. ponyconf+{token}@exemple.com)."
+msgstr ""
+"L’adresse de réponse doit être une chaine de texte formatable avec un "
+"argument « token » (e.g. ponyconf+{token}@exemple.com)."
+
+#: cfp/models.py:83
+msgid "Your Name"
+msgstr "Votre Nom"
+
+#: cfp/models.py:100
+msgid "This field is only visible by organizers."
+msgstr "Ce champs est uniquement visible par les organisateurs."
+
+#: cfp/models.py:187 proposals/models.py:96
+msgid "Default duration (min)"
+msgstr "Durée par défaut (min)"
+
+#: cfp/models.py:188 proposals/models.py:97
+msgid "Color on program"
+msgstr "Couleur sur le programme"
+
+#: cfp/models.py:189 proposals/models.py:98
+msgid "Label on program"
+msgstr "Label dans le xml du programme"
+
+#: cfp/models.py:262 cfp/templates/cfp/staff/base.html:18
+#: cfp/templates/cfp/staff/participant_list.html:8
+#: cfp/templates/cfp/staff/talk_details.html:65
+#: cfp/templates/cfp/staff/talk_list.html:42 proposals/models.py:154
+#: proposals/templates/proposals/speaker_list.html:9
+#: proposals/templates/proposals/talk_detail.html:76
+#: proposals/templates/proposals/talk_list.html:49
+msgid "Speakers"
+msgstr "Orateurs"
+
+#: cfp/models.py:263
+msgid "Talk Title"
+msgstr "Titre de votre proposition:"
+
+#: cfp/models.py:266
+msgid "Description of your talk"
+msgstr "Description de votre proposition"
+
+#: cfp/models.py:268
+msgid "Message to organizers"
+msgstr "Message aux organisateurs"
+
+#: cfp/models.py:268
+msgid ""
+"If you have any constraint or if you have anything that may help you to "
+"select your talk, like a video or slides of your talk, please write it down "
+"here"
+msgstr ""
+"Si vous avez une contrainte, ou un élément qui nous aide à sélectionner "
+"votre proposition, comme une vidéo, des slides, n'hésitez pas à les ajouter "
+"ici."
+
+#: cfp/models.py:269
+msgid "Talk Category"
+msgstr "Catégorie de proposition"
+
+#: cfp/models.py:270
+msgid "I'm ok to be recorded on video"
+msgstr "J’accepte d’être enregistré en vidéo"
+
+#: cfp/models.py:271
+msgid "Video licence"
+msgstr "Licence vidéo"
+
+#: cfp/models.py:272
+msgid "I need sound"
+msgstr "J’ai besoin de son"
+
+#: cfp/models.py:274
+msgid "Beginning date and time"
+msgstr "Date et heure de début"
+
+#: cfp/models.py:275 proposals/models.py:165
+msgid "Duration (min)"
+msgstr "Durée (min)"
+
+#: cfp/signals.py:79
+#, python-format
+msgid "[%(prefix)s] Message from the staff"
+msgstr "[%(prefix)s] Message du staff"
+
+#: cfp/signals.py:80
+#, python-format
+msgid "[%(prefix)s] Conversation with %(dest)s"
+msgstr "[%(prefix)s] Conversation avec %(dest)s"
+
+#: cfp/signals.py:96
+#, python-format
+msgid "[%(prefix)s] Talk: %(talk)s"
+msgstr "[%(prefix)s] Talk: %(talk)s"
+
+#: cfp/templates/cfp/closed.html:9 cfp/templates/cfp/propose.html:11
+#: cfp/templates/cfp/speaker.html:11
+#: proposals/templates/proposals/participate.html:9
+msgid "Participate"
+msgstr "Participer"
+
+#: cfp/templates/cfp/closed.html:13
+msgid "Sorry, the Call for Participation is closed!"
+msgstr "Désolé, l’appel à participation est fermé."
+
+#: cfp/templates/cfp/complete.html:10
+msgid "Your proposition have been successfully submitted!"
+msgstr "Votre proposition a été transmise avec succès !"
+
+#: cfp/templates/cfp/complete.html:16
+msgid "Thanks for your proposal"
+msgstr "Merci pour votre proposition"
+
+#: cfp/templates/cfp/complete.html:17
+msgid "You can at anytime:"
+msgstr "Vous pouvez à tout moment :"
+
+#: cfp/templates/cfp/complete.html:19
+msgid "Edit your talk:"
+msgstr "Éditer un exposé"
+
+#: cfp/templates/cfp/complete.html:20
+msgid "Add an additionnal speaker:"
+msgstr "Ajouter un co-speaker :"
+
+#: cfp/templates/cfp/complete.html:21
+msgid "Edit your profile:"
+msgstr "Éditer votre profil :"
+
+#: cfp/templates/cfp/complete.html:24
+msgid "An email has been sent to you with those URLs"
+msgstr "Un mail vous a été envoyé avec toutes les URLs"
+
+#: cfp/templates/cfp/propose.html:22 cfp/templates/cfp/speaker.html:21
+#: cfp/templates/cfp/staff/conference.html:14
+#: cfp/templates/cfp/staff/create_user.html:13
+msgid "Save"
msgstr "Envoyer"
-#: conversations/templates/conversations/conversation.html:9
-#, python-format
-msgid "Conversation with %(correspondent)s"
-msgstr "Conversation avec %(correspondent)s"
+#: cfp/templates/cfp/staff/base.html:19
+#: cfp/templates/cfp/staff/track_list.html:9
+#: proposals/templates/proposals/track_list.html:9
+msgid "Tracks"
+msgstr "Sessions"
-#: conversations/templates/conversations/correspondent_list.html:9
-#: ponyconf/templates/staff.html:17
-msgid "Correspondents"
-msgstr "Correspondants"
+#: cfp/templates/cfp/staff/base.html:20
+#: cfp/templates/cfp/staff/room_list.html:9
+#: planning/templates/planning/room_list.html:9
+msgid "Rooms"
+msgstr "Salles"
-#: conversations/templates/conversations/correspondent_list.html:10
-msgid "This is the list of participants that you follow."
-msgstr "Ceci est la liste des participants que vous suivez."
+#: cfp/templates/cfp/staff/base.html:21
+#: cfp/templates/cfp/staff/conference.html:8
+msgid "Conference"
+msgstr "Conférence"
-#: conversations/templates/conversations/inbox.html:9
+#: cfp/templates/cfp/staff/base.html:29
+msgid "Please select a category."
+msgstr "Veuillez sélectionner une catégorie."
+
+#: cfp/templates/cfp/staff/conference.html:13
+#: cfp/templates/cfp/staff/create_user.html:8
+msgid "Add a new user"
+msgstr "Ajouter un nouvel utilisateur"
+
+#: cfp/templates/cfp/staff/participant_details.html:20
+msgid "Informations"
+msgstr "Informations"
+
+#: cfp/templates/cfp/staff/participant_details.html:22
+msgid "E-mail:"
+msgstr "E-mail :"
+
+#: cfp/templates/cfp/staff/participant_details.html:23
+msgid "Twitter:"
+msgstr "Twitter :"
+
+#: cfp/templates/cfp/staff/participant_details.html:24
+msgid "LinkedIn:"
+msgstr "LinkedIn :"
+
+#: cfp/templates/cfp/staff/participant_details.html:25
+msgid "Github:"
+msgstr "Github :"
+
+#: cfp/templates/cfp/staff/participant_details.html:26
+msgid "Website:"
+msgstr "Website :"
+
+#: cfp/templates/cfp/staff/participant_details.html:27
+msgid "Facebook:"
+msgstr "Facebook :"
+
+#: cfp/templates/cfp/staff/participant_details.html:28
+msgid "Mastodon:"
+msgstr "Mastodon :"
+
+#: cfp/templates/cfp/staff/participant_details.html:29
+msgid "Phone number:"
+msgstr "Numéro de téléphone :"
+
+#: cfp/templates/cfp/staff/participant_details.html:30
+msgid "Language:"
+msgstr "Langue :"
+
+#: cfp/templates/cfp/staff/participant_details.html:40
+#: proposals/templates/proposals/_talk_list.html:8
+msgid "by"
+msgstr "par"
+
+#: cfp/templates/cfp/staff/participant_details.html:43
+#: cfp/templates/cfp/staff/room_details.html:21
+#: cfp/templates/cfp/staff/room_details.html:39
+#: cfp/templates/cfp/staff/talk_list.html:59
+#: proposals/templates/proposals/_talk_list.html:11
+#: proposals/templates/proposals/_talk_list.html:17
+#: proposals/templates/proposals/talk_list.html:66
+msgid "and"
+msgstr "et"
+
+#: cfp/templates/cfp/staff/participant_details.html:46
+#: proposals/templates/proposals/_talk_list.html:14
+msgid "in"
+msgstr "dans la session"
+
+#: cfp/templates/cfp/staff/participant_details.html:52
+#: proposals/templates/proposals/_talk_list.html:23
+msgid "No talks"
+msgstr "Aucun exposé"
+
+#: cfp/templates/cfp/staff/participant_details.html:55
+#: cfp/templates/cfp/staff/talk_details.html:114
msgid "Messaging"
msgstr "Messagerie"
-#: conversations/templates/conversations/inbox.html:10
-msgid "You can use this page to communicate with the staff."
+#: cfp/templates/cfp/staff/participant_details.html:59
+msgid ""
+"Send a message – this message will be received by this participant and "
+"all the staff team "
msgstr ""
-"Vous pouvez utiliser cette page pour communiquer avec l’équipe organisatrice."
+"Envoyer un message – ce message sera reçu par le participant et l’équipe "
+"d’organisation "
-#: conversations/views.py:40
-msgid "Message sent!"
-msgstr "Message envoyé !"
+#: cfp/templates/cfp/staff/participant_form.html:8
+msgid "Edit a speaker"
+msgstr "Éditer un orateur"
-#: planning/templates/planning/public-program.html:8
-#: planning/templates/planning/schedule.html:9 ponyconf/templates/staff.html:15
-msgid "Schedule"
-msgstr "Programme"
+#: cfp/templates/cfp/staff/participant_list.html:45
+#: cfp/templates/cfp/staff/talk_list.html:35
+#: proposals/templates/proposals/speaker_list.html:44
+#: proposals/templates/proposals/talk_list.html:43
+#: volunteers/templates/volunteers/volunteer_list.html:25
+msgid "Total:"
+msgstr "Total :"
-#: planning/templates/planning/room_detail.html:13
+#: cfp/templates/cfp/staff/participant_list.html:45
+#: proposals/templates/proposals/speaker_list.html:44
+msgid "speaker"
+msgstr "orateur"
+
+#: cfp/templates/cfp/staff/participant_list.html:50
+#: proposals/templates/proposals/speaker_list.html:50
+msgid "Talk count"
+msgstr "Nombre d’exposé"
+
+#: cfp/templates/cfp/staff/participant_list.html:70
+#, python-format
+msgid "accepted: %(accepted)s"
+msgid_plural "accepted: %(accepted)s"
+msgstr[0] "accepté : %(accepted)s"
+msgstr[1] "acceptés : %(accepted)s"
+
+#: cfp/templates/cfp/staff/participant_list.html:72
+#, python-format
+msgid "pending: %(pending)s"
+msgid_plural "pending: %(pending)s"
+msgstr[0] "en attente : %(pending)s"
+msgstr[1] "en attente : %(pending)s"
+
+#: cfp/templates/cfp/staff/participant_list.html:74
+#, python-format
+msgid "refused: %(refused)s"
+msgid_plural "refused: %(refused)s"
+msgstr[0] "refusé : %(refused)s"
+msgstr[1] "refusés : %(refused)s"
+
+#: cfp/templates/cfp/staff/room_details.html:13
+#: planning/templates/planning/room_detail.html:14
msgid "Scheduled talks"
msgstr "Exposés planifiés"
-#: planning/templates/planning/room_detail.html:22
-#: planning/templates/planning/room_detail.html:33
+#: cfp/templates/cfp/staff/room_details.html:28
+#: cfp/templates/cfp/staff/room_details.html:45
+#: planning/templates/planning/room_detail.html:23
+#: planning/templates/planning/room_detail.html:34
msgid "No talks."
msgstr "Aucun exposé."
-#: planning/templates/planning/room_detail.html:25
+#: cfp/templates/cfp/staff/room_details.html:31
+#: planning/templates/planning/room_detail.html:26
msgid "Unscheduled talks"
msgstr "Exposés non planifiés"
-#: planning/templates/planning/room_form.html:14
-msgid "Modify the room"
-msgstr "Modifier la salle"
-
+#: cfp/templates/cfp/staff/room_list.html:11
#: planning/templates/planning/room_form.html:14
#: planning/templates/planning/room_list.html:12
msgid "Add a room"
msgstr "Ajouter une salle"
-#: planning/templates/planning/room_list.html:9
-#: ponyconf/templates/staff.html:14
-msgid "Rooms"
-msgstr "Salles"
-
-#: planning/templates/planning/room_list.html:21
+#: cfp/templates/cfp/staff/room_list.html:21
+#: planning/templates/planning/room_list.html:24
msgid "place"
msgstr "place"
-#: planning/templates/planning/room_list.html:24
+#: cfp/templates/cfp/staff/room_list.html:23
+#: planning/templates/planning/room_list.html:27
msgid "Some talks are not scheduled yet."
msgstr "Certains exposés ne sont pas encore planifiés."
-#: planning/templates/planning/room_list.html:25
-#: proposals/templates/proposals/talk_list.html:42
+#: cfp/templates/cfp/staff/room_list.html:24
+#: cfp/templates/cfp/staff/talk_list.html:35
+#: cfp/templates/cfp/staff/track_list.html:21
+#: planning/templates/planning/room_list.html:28
+#: proposals/templates/proposals/talk_list.html:43
#: proposals/templates/proposals/topic_list.html:23
#: proposals/templates/proposals/track_list.html:23
msgid "talk"
msgstr "exposé"
-#: planning/templates/planning/room_list.html:37
+#: cfp/templates/cfp/staff/room_list.html:33
+#: planning/templates/planning/room_list.html:40
msgid "No rooms."
msgstr "Aucune salle."
-#: ponyconf/settings.py:141
+#: cfp/templates/cfp/staff/talk_decide.html:8
+#: proposals/templates/proposals/talk_decide.html:9
+msgid "Are you sure to accept this proposals?"
+msgstr "Êtes-vous sûr d’accepter cette propositon d’intervention ?"
+
+#: cfp/templates/cfp/staff/talk_decide.html:8
+#: proposals/templates/proposals/talk_decide.html:9
+msgid "Are you sure to decline this proposals?"
+msgstr "Êtes-vous sûr de décliner cette propositon d’intervention ?"
+
+#: cfp/templates/cfp/staff/talk_decide.html:10
+#: proposals/templates/proposals/talk_decide.html:11
+msgid "Information about the proposals"
+msgstr "Information sur la propositon d’intervention"
+
+#: cfp/templates/cfp/staff/talk_decide.html:11
+#: proposals/templates/proposals/talk_decide.html:12
+msgid "Title:"
+msgstr "Titre :"
+
+#: cfp/templates/cfp/staff/talk_decide.html:12
+#: proposals/templates/proposals/talk_decide.html:13
+msgid "Kind:"
+msgstr "Type d’intervention :"
+
+#: cfp/templates/cfp/staff/talk_decide.html:14
+#: proposals/templates/proposals/talk_decide.html:15
+msgid "Information for the proposer"
+msgstr "Information à destination de l’auteur de la proposition"
+
+#: cfp/templates/cfp/staff/talk_decide.html:18
+#: proposals/templates/proposals/talk_decide.html:19
+msgid ""
+"If you want to send a message to the proposer, please enter it below. "
+"Remember to indicate which talk your message is reffering."
+msgstr ""
+"Si vous souhaitez envoyer un message à l’auteur de la proposition, saisissez-"
+"le ci-dessous. N’oubliez pas de spécifier à quelle proposition "
+"d’intervention votre message fait référence."
+
+#: cfp/templates/cfp/staff/talk_decide.html:21
+#: proposals/templates/proposals/talk_decide.html:22
+msgid "Accept the proposal"
+msgstr "Accepter la proposition"
+
+#: cfp/templates/cfp/staff/talk_decide.html:21
+#: proposals/templates/proposals/talk_decide.html:22
+msgid "Decline the proposal"
+msgstr "Décliner la proposition"
+
+#: cfp/templates/cfp/staff/talk_details.html:25
+msgctxt "session"
+msgid "No assigned yet."
+msgstr "Pas encore assignée."
+
+#: cfp/templates/cfp/staff/talk_details.html:28
+msgid "Timeslot"
+msgstr "Créneau"
+
+#: cfp/templates/cfp/staff/talk_details.html:32
+#: cfp/templates/cfp/staff/talk_details.html:40
+#: proposals/templates/proposals/talk_detail.html:44
+#: proposals/templates/proposals/talk_detail.html:52
+msgid "not defined"
+msgstr "non défini"
+
+#: cfp/templates/cfp/staff/talk_details.html:63
+#: proposals/templates/proposals/talk_detail.html:74
+msgid "No description provided."
+msgstr "Aucune description fournie."
+
+#: cfp/templates/cfp/staff/talk_details.html:72
+#: proposals/templates/proposals/talk_detail.html:83
+msgid "No speakers."
+msgstr "Aucun orateur."
+
+#: cfp/templates/cfp/staff/talk_details.html:77
+#: proposals/templates/proposals/talk_detail.html:104
+msgid "No notes."
+msgstr "Aucune note."
+
+#: cfp/templates/cfp/staff/talk_details.html:79
+#: proposals/templates/proposals/talk_detail.html:108
+msgid "Moderation"
+msgstr "Modération"
+
+#: cfp/templates/cfp/staff/talk_details.html:93
+#: proposals/templates/proposals/talk_detail.html:124
+msgid "vote"
+msgstr "vote"
+
+#: cfp/templates/cfp/staff/talk_details.html:93
+#: proposals/templates/proposals/talk_detail.html:124
+msgid "average:"
+msgstr "moyenne :"
+
+#: cfp/templates/cfp/staff/talk_details.html:118
+msgid ""
+"Comment this talk – this message will be received by the staff team "
+"only "
+msgstr ""
+"Commenter cette proposition – ce message sera reçu uniquement par "
+"l’équipe d’organisation "
+
+#: cfp/templates/cfp/staff/talk_form.html:8
+#: proposals/templates/proposals/talk_edit.html:10
+msgid "Edit a talk"
+msgstr "Éditer un exposé"
+
+#: cfp/templates/cfp/staff/talk_list.html:10
+#: proposals/templates/proposals/speaker_list.html:11
+#: proposals/templates/proposals/talk_list.html:11
+#: volunteers/templates/volunteers/volunteer_list.html:11
+msgid "Show filtering options…"
+msgstr "Afficher les options de filtrage…"
+
+#: cfp/templates/cfp/staff/talk_list.html:29
+#: proposals/templates/proposals/speaker_list.html:38
+#: proposals/templates/proposals/talk_list.html:35
+#: volunteers/templates/volunteers/volunteer_list.html:19
+msgid "Filter"
+msgstr "Filtrer"
+
+#: cfp/templates/cfp/staff/talk_list.html:41 proposals/models.py:162
+#: proposals/templates/proposals/talk_list.html:48
+msgid "Intervention kind"
+msgstr "Type d’intervention"
+
+#: cfp/templates/cfp/staff/talk_list.html:70
+#: proposals/templates/proposals/talk_list.html:82
+#, python-format
+msgid "Pending, score: %(score)s"
+msgstr "En cours, score : %(score)s"
+
+#: cfp/templates/cfp/staff/track_list.html:11
+#: proposals/templates/proposals/track_list.html:12
+msgid "Add a track"
+msgstr "Ajouter une session"
+
+#: cfp/templates/cfp/staff/track_list.html:19
+#: proposals/templates/proposals/track_list.html:21
+msgid "manager"
+msgstr "responsable"
+
+#: cfp/templates/cfp/staff/track_list.html:31
+#: proposals/templates/proposals/track_list.html:33
+msgid "No tracks."
+msgstr "Aucune session."
+
+#: cfp/views.py:68
+msgid ""
+"Hi {},\n"
+"\n"
+"Your talk has been submitted for {}.\n"
+"\n"
+"Here are the details of your talk:\n"
+"Title: {}\n"
+"Description: {}\n"
+"\n"
+"You can at anytime:\n"
+"- edit your talk: {}\n"
+"- add a new co-speaker: {}\n"
+"- edit your profile: {}\n"
+"\n"
+"If you have any question, your can answer to this email.\n"
+"\n"
+"Thanks!\n"
+"\n"
+"{}\n"
+"\n"
+msgstr ""
+"Bonjour {},\n"
+"\n"
+"Votre proposition a été transmise à {}.\n"
+"\n"
+"Vous trouverez ci-dessous les détails de votre proposition:\n"
+"Titre: {}\n"
+"Description: {}\n"
+"\n"
+"Vous pouvez à tout moment:\n"
+"- éditer votre proposition: {}\n"
+"- ajouter un co-speaker: {}\n"
+"- éditer votre profil: {}\n"
+"\n"
+"Si vous avez une question, vous pouvez répondre à ce mail.\n"
+"\n"
+"{}\n"
+"\n"
+
+#: cfp/views.py:224 cfp/views.py:286
+msgid "Message sent!"
+msgstr "Message envoyé !"
+
+#: cfp/views.py:237 proposals/views.py:321
+msgid "Vote successfully created"
+msgstr "A voté !"
+
+#: cfp/views.py:237 proposals/views.py:321
+msgid "Vote successfully updated"
+msgstr "Vote mis à jour"
+
+#: cfp/views.py:252
+msgid "The talk has been accepted."
+msgstr "L’exposé a été accepté."
+
+#: cfp/views.py:254
+msgid "The talk has been declined."
+msgstr "L’exposé a été décliné."
+
+#: cfp/views.py:258 proposals/views.py:347
+msgid "Decision taken in account"
+msgstr "Décision enregistrée"
+
+#: cfp/views.py:314
+msgid "[{}] You have been added to the staff team"
+msgstr "[{}] Vous avez été ajouté aux membres du staff"
+
+#: cfp/views.py:315
+msgid ""
+"Hi {},\n"
+"\n"
+"You have been added to the staff team.\n"
+"\n"
+"You can now:\n"
+"- login: {}\n"
+"- reset your password: {}\n"
+"\n"
+"{}\n"
+"\n"
+msgstr ""
+"Bonjour {},\n"
+"\n"
+"Vous avez été ajouté à l’équipe d’organisation.\n"
+"\n"
+"Vous pouvez à présent :\n"
+"- vous connecter : {}\n"
+"- réinitialiser votre mot de passe : {}\n"
+"\n"
+"{}\n"
+"\n"
+
+#: cfp/views.py:336
+msgid "Modifications successfully saved."
+msgstr "Modification enregistrée avec succès."
+
+#: cfp/views.py:428
+msgid "User created successfully."
+msgstr "Utilisateur créé avec succès."
+
+#: mailing/models.py:93
+#, python-format
+msgid "Message from %(author)s"
+msgstr "Message de %(author)s"
+
+#: mailing/templates/mailing/_message_form.html:13
+msgid "Send"
+msgstr "Envoyer"
+
+#: mailing/templates/mailing/_message_list.html:13
+msgid "No messages."
+msgstr "Aucun message."
+
+#: planning/templates/planning/public-program.html:8
+#: planning/templates/planning/schedule.html:9
+msgid "Schedule"
+msgstr "Programme"
+
+#: planning/templates/planning/room_form.html:14
+msgid "Modify the room"
+msgstr "Modifier la salle"
+
+#: ponyconf/settings.py:145
msgid "English"
msgstr "Anglais"
-#: ponyconf/settings.py:142
+#: ponyconf/settings.py:146
msgid "French"
msgstr "Français"
-#: ponyconf/templates/base.html:20
+#: ponyconf/templates/base.html:21
msgid "Home"
msgstr "Accueil"
-#: ponyconf/templates/base.html:22
-msgid "Exhibitor"
-msgstr "Exposant"
-
#: ponyconf/templates/base.html:23
-msgid "Volunteer"
-msgstr "Bénévole"
+msgid "Call for participation"
+msgstr "Appel à participation"
-#: ponyconf/templates/base.html:25
-msgid "Workshop registration"
-msgstr "Inscription aux ateliers"
-
-#: ponyconf/templates/base.html:30
+#: ponyconf/templates/base.html:36 ponyconf/templates/base.html:50
msgid "Staff"
msgstr "Staff"
-#: ponyconf/templates/base.html:36
-#: proposals/templates/proposals/talk_registrable_list.html:25
-msgid "Register"
-msgstr "S’inscrire"
+#: ponyconf/templates/base.html:48
+msgid "Logout"
+msgstr "Déconnection"
-#: ponyconf/templates/base.html:54
+#: ponyconf/templates/base.html:67
msgid "Powered by"
msgstr "Propulsé par"
-#: ponyconf/templates/staff.html:9 proposals/models.py:159
-#: proposals/templates/proposals/talk_detail.html:26
-#: proposals/templates/proposals/talk_list.html:49
-#: proposals/templates/proposals/topic_list.html:9
-msgid "Topics"
-msgstr "Thèmes"
+#: ponyconf/templates/registration/login.html:10
+msgid "Staff area"
+msgstr "Espace organisateurs"
-#: ponyconf/templates/staff.html:10
-#: proposals/templates/proposals/track_list.html:9
-msgid "Tracks"
-msgstr "Sessions"
+#: ponyconf/templates/registration/login.html:12
+msgid "Please login to access staff area."
+msgstr "Veuillez vous connecter pour accéder à l’espace organisateurs."
-#: ponyconf/templates/staff.html:12 proposals/models.py:154
-#: proposals/templates/proposals/speaker_list.html:9
-#: proposals/templates/proposals/talk_detail.html:72
-#: proposals/templates/proposals/talk_list.html:48
-msgid "Speakers"
-msgstr "Orateurs"
+#: ponyconf/templates/registration/login.html:24
+msgid "Login"
+msgstr "Se connecter"
-#: ponyconf/templates/staff.html:13
-#: volunteers/templates/volunteers/volunteer_list.html:9
-msgid "Volunteers"
-msgstr "Bénévoles"
+#: ponyconf/templates/registration/login.html:25
+msgid "Forgot password?"
+msgstr "Mot de passe oublié ?"
-#: ponyconf/templates/staff.html:18
-msgid "Conference"
-msgstr "Conférence"
+#: ponyconf/templates/registration/password_change_form.html:9
+msgid "Password Change"
+msgstr "Changement de mot de passe"
+
+#: ponyconf/urls.py:27
+msgid "Email address"
+msgstr "Adresse e-mail"
#: proposals/forms.py:46
msgid "Should be less than 255 characters"
@@ -506,62 +1096,42 @@ msgstr "Texte court, moins de 255 caractères"
msgid "If you want to add some precisions for the organizers."
msgstr "Si vous souhaitez apporter des précisions à l'équipe d'organisation."
-#: proposals/forms.py:72
-msgid "Filter talks you already / not yet voted for"
-msgstr ""
-"Filtrer les propositions pour lesquelles vous avez déjà voté / pas encore "
-"voté"
-
-#: proposals/forms.py:73
-msgid "Filter talks already / not yet affected to a room"
-msgstr "Filtrer les exposés déjà / pas encore affectées à une salle"
-
-#: proposals/forms.py:74
-msgid "Filter talks already / not yet scheduled"
-msgstr "Filtrer les exposés déjà / pas encore planifiées"
-
#: proposals/forms.py:75
msgid "Filter talks with / without materials"
msgstr "Filtrer les exposés avec / sans supports"
-#: proposals/forms.py:90
+#: proposals/forms.py:76
+msgid "Filter talks with / without video"
+msgstr "Filtrer les exposés avec / sans vidéo"
+
+#: proposals/forms.py:91
msgid "Accept talk?"
msgstr "Accepter la proposition ?"
-#: proposals/forms.py:91
+#: proposals/forms.py:92
msgid "Assign to a track"
msgstr "Assigner à une session"
-#: proposals/forms.py:92
+#: proposals/forms.py:93
msgid "Put in a room"
msgstr "Assigner à une salle"
-#: proposals/forms.py:194
+#: proposals/forms.py:195
msgid "Name or nickname"
msgstr "Nom ou pseudo"
-#: proposals/forms.py:195
+#: proposals/forms.py:196
msgid "How much is 3+4?"
msgstr "Combien font 3+4 ?"
-#: proposals/forms.py:195
+#: proposals/forms.py:196
msgid "Anti-bot"
msgstr "Anti-robot"
-#: proposals/forms.py:200
+#: proposals/forms.py:201
msgid "Please re-do the maths."
msgstr "Refaites les calculs."
-#: proposals/models.py:52 proposals/models.py:75 proposals/models.py:132
-#: volunteers/models.py:12
-msgid "Name"
-msgstr "Nom"
-
-#: proposals/models.py:54 proposals/models.py:77 proposals/models.py:158
-#: proposals/templates/proposals/talk_detail.html:68 volunteers/models.py:14
-msgid "Description"
-msgstr "Description"
-
#: proposals/models.py:56
msgid "Managers"
msgstr "Responsables"
@@ -574,26 +1144,10 @@ msgstr "Session de destination"
msgid "Reviewers"
msgstr "Responsables"
-#: proposals/models.py:96
-msgid "Default duration (min)"
-msgstr "Durée par défaut (min)"
-
-#: proposals/models.py:97
-msgid "Color on program"
-msgstr "Couleur sur le programme"
-
-#: proposals/models.py:98
-msgid "Label on program"
-msgstr "Label dans le xml du programme"
-
#: proposals/models.py:139
msgid "Email"
msgstr "E-mail"
-#: proposals/models.py:155 proposals/templates/proposals/talk_list.html:46
-msgid "Title"
-msgstr "Titre"
-
#: proposals/models.py:155
msgid "After submission, title can only be changed by the staff."
msgstr ""
@@ -604,26 +1158,17 @@ msgstr ""
msgid "Abstract"
msgstr "Résumé"
+#: proposals/models.py:159 proposals/templates/proposals/talk_detail.html:26
+#: proposals/templates/proposals/talk_list.html:50
+#: proposals/templates/proposals/topic_list.html:9
+msgid "Topics"
+msgstr "Thèmes"
+
#: proposals/models.py:159
msgid "The topics can not be changed after submission."
msgstr "Les thèmes ne peuvent pas être modifiés après soumission."
-#: proposals/models.py:160 proposals/templates/proposals/talk_detail.html:33
-#: proposals/templates/proposals/talk_detail.html:85
-#: proposals/templates/proposals/talk_list.html:50
-#: proposals/templates/proposals/track_form.html:14
-msgid "Track"
-msgstr "Session"
-
-#: proposals/models.py:162 proposals/templates/proposals/talk_list.html:47
-msgid "Intervention kind"
-msgstr "Type d’intervention"
-
-#: proposals/models.py:165
-msgid "Duration (min)"
-msgstr "Durée (min)"
-
-#: proposals/models.py:169 proposals/templates/proposals/talk_detail.html:127
+#: proposals/models.py:169 proposals/templates/proposals/talk_detail.html:131
msgid "Attendees"
msgstr "Inscrits"
@@ -662,32 +1207,10 @@ msgstr "stand"
msgid "other"
msgstr "autre"
-#: proposals/templates/proposals/_talk_list.html:8
-msgid "by"
-msgstr "par"
-
-#: proposals/templates/proposals/_talk_list.html:11
-#: proposals/templates/proposals/_talk_list.html:17
-#: proposals/templates/proposals/talk_list.html:65
-msgid "and"
-msgstr "et"
-
-#: proposals/templates/proposals/_talk_list.html:14
-msgid "in"
-msgstr "portant sur"
-
-#: proposals/templates/proposals/_talk_list.html:23
-msgid "No talks"
-msgstr "Aucun exposé"
-
#: proposals/templates/proposals/conference.html:11
msgid "Home page"
msgstr "Page d’accueil"
-#: proposals/templates/proposals/participate.html:9
-msgid "Participate"
-msgstr "Participer"
-
#: proposals/templates/proposals/participate.html:12
msgid "My talks:"
msgstr "Mes exposés :"
@@ -716,28 +1239,6 @@ msgstr "Proposer un exposé"
msgid "Sorry, the Call for Participation is closed."
msgstr "Désolé, l’appel à participation est fermé."
-#: proposals/templates/proposals/speaker_list.html:11
-#: proposals/templates/proposals/talk_list.html:11
-#: volunteers/templates/volunteers/volunteer_list.html:11
-msgid "Show filtering options…"
-msgstr "Afficher les options de filtrage…"
-
-#: proposals/templates/proposals/speaker_list.html:38
-#: proposals/templates/proposals/talk_list.html:34
-#: volunteers/templates/volunteers/volunteer_list.html:19
-msgid "Filter"
-msgstr "Filtrer"
-
-#: proposals/templates/proposals/speaker_list.html:44
-#: proposals/templates/proposals/talk_list.html:42
-#: volunteers/templates/volunteers/volunteer_list.html:25
-msgid "Total:"
-msgstr "Total :"
-
-#: proposals/templates/proposals/speaker_list.html:44
-msgid "speaker"
-msgstr "orateur"
-
#: proposals/templates/proposals/speaker_list.html:48
#: volunteers/templates/volunteers/volunteer_list.html:29
msgid "Username"
@@ -748,10 +1249,6 @@ msgstr "Nom d’utilisateur"
msgid "Fullname"
msgstr "Prénom et nom"
-#: proposals/templates/proposals/speaker_list.html:50
-msgid "Talk count"
-msgstr "Nombre d’exposé"
-
#: proposals/templates/proposals/speaker_list.html:51
msgctxt "table column title"
msgid "Need transport?"
@@ -774,47 +1271,6 @@ msgstr "Contacter :"
msgid "link"
msgstr "lien"
-#: proposals/templates/proposals/talk_decide.html:9
-msgid "Are you sure to accept this proposals?"
-msgstr "Êtes-vous sûr d’accepter cette propositon d’intervention ?"
-
-#: proposals/templates/proposals/talk_decide.html:9
-msgid "Are you sure to decline this proposals?"
-msgstr "Êtes-vous sûr de décliner cette propositon d’intervention ?"
-
-#: proposals/templates/proposals/talk_decide.html:11
-msgid "Information about the proposals"
-msgstr "Information sur la propositon d’intervention"
-
-#: proposals/templates/proposals/talk_decide.html:12
-msgid "Title:"
-msgstr "Titre :"
-
-#: proposals/templates/proposals/talk_decide.html:13
-msgid "Kind:"
-msgstr "Type d’intervention :"
-
-#: proposals/templates/proposals/talk_decide.html:15
-msgid "Information for the proposer"
-msgstr "Information à destination de l’auteur de la proposition"
-
-#: proposals/templates/proposals/talk_decide.html:19
-msgid ""
-"If you want to send a message to the proposer, please enter it below. "
-"Remember to indicate which talk your message is reffering."
-msgstr ""
-"Si vous souhaitez envoyer un message à l’auteur de la proposition, saisissez-"
-"le ci-dessous. N’oubliez pas de spécifier à quelle proposition "
-"d’intervention votre message fait référence."
-
-#: proposals/templates/proposals/talk_decide.html:22
-msgid "Accept the proposal"
-msgstr "Accepter la proposition"
-
-#: proposals/templates/proposals/talk_decide.html:22
-msgid "Decline the proposal"
-msgstr "Décliner la proposition"
-
#: proposals/templates/proposals/talk_detail.html:19
msgid "No abstract provided."
msgstr "Aucun résumé fourni."
@@ -829,15 +1285,10 @@ msgid "No topics."
msgstr "Aucun thème."
#: proposals/templates/proposals/talk_detail.html:37
-#: proposals/templates/proposals/talk_detail.html:86
+#: proposals/templates/proposals/talk_detail.html:90
msgid "No assigned yet."
msgstr "Pas encore assigné."
-#: proposals/templates/proposals/talk_detail.html:44
-#: proposals/templates/proposals/talk_detail.html:52
-msgid "not defined"
-msgstr "non défini"
-
#: proposals/templates/proposals/talk_detail.html:56
msgid "Registrations"
msgstr "Inscriptions"
@@ -846,77 +1297,35 @@ msgstr "Inscriptions"
msgid "required but unlimited"
msgstr "requis mais non limité"
-#: proposals/templates/proposals/talk_detail.html:70
-msgid "No description provided."
-msgstr "Aucune description fournie."
+#: proposals/templates/proposals/talk_detail.html:64
+msgid "Video"
+msgstr "Vidéo"
-#: proposals/templates/proposals/talk_detail.html:79
-msgid "No speakers."
-msgstr "Aucun orateur."
+#: proposals/templates/proposals/talk_detail.html:65
+msgid "download"
+msgstr "Télécharger"
-#: proposals/templates/proposals/talk_detail.html:90
+#: proposals/templates/proposals/talk_detail.html:94
msgid "Assign to"
msgstr "Assigner à"
-#: proposals/templates/proposals/talk_detail.html:100
-msgid "No notes."
-msgstr "Aucune note."
-
-#: proposals/templates/proposals/talk_detail.html:104
-msgid "Moderation"
-msgstr "Modération"
-
-#: proposals/templates/proposals/talk_detail.html:106
-#: proposals/templates/proposals/talk_list.html:51
-msgid "Status"
-msgstr "Statut"
-
-#: proposals/templates/proposals/talk_detail.html:111
-msgid "Vote"
-msgstr "Vote"
-
-#: proposals/templates/proposals/talk_detail.html:120
-msgid "vote"
-msgstr "vote"
-
-#: proposals/templates/proposals/talk_detail.html:120
-msgid "average:"
-msgstr "moyenne :"
-
-#: proposals/templates/proposals/talk_detail.html:134
+#: proposals/templates/proposals/talk_detail.html:138
msgid "No attendees yet."
msgstr "Il n’y a pas encore d’inscrit."
-#: proposals/templates/proposals/talk_detail.html:139
+#: proposals/templates/proposals/talk_detail.html:143
msgid "Messages"
msgstr "Messages"
-#: proposals/templates/proposals/talk_detail.html:140
+#: proposals/templates/proposals/talk_detail.html:144
msgid "These messages are for organization team only."
msgstr "Ces messages sont à destination de la team d’organisation seulement."
-#: proposals/templates/proposals/talk_edit.html:10
-msgid "Edit a talk"
-msgstr "Éditer un exposé"
-
-#: proposals/templates/proposals/talk_list.html:77
-msgid "Accepted"
-msgstr "Accepté"
-
-#: proposals/templates/proposals/talk_list.html:79
-msgid "Declined"
-msgstr "Décliné"
-
-#: proposals/templates/proposals/talk_list.html:81
-#, python-format
-msgid "Pending, score: %(score)s"
-msgstr "En cours, score : %(score)s"
-
-#: proposals/templates/proposals/talk_list.html:94
+#: proposals/templates/proposals/talk_list.html:95
msgid "For selected talks:"
msgstr "Pour les exposés sélectionnés :"
-#: proposals/templates/proposals/talk_list.html:102
+#: proposals/templates/proposals/talk_list.html:103
msgid "Apply"
msgstr "Appliquer"
@@ -943,6 +1352,10 @@ msgstr[1] "%(remaining)s places restantes"
msgid "Unregister"
msgstr "Se désinscrire"
+#: proposals/templates/proposals/talk_registrable_list.html:25
+msgid "Register"
+msgstr "S’inscrire"
+
#: proposals/templates/proposals/talk_registrable_list.html:33
msgid ""
"There are no workshops requiring registration for now … come back later!"
@@ -962,58 +1375,34 @@ msgstr "Ajouter un thème"
msgid "reviewer"
msgstr "Responsable"
-#: proposals/templates/proposals/track_list.html:12
-msgid "Add a track"
-msgstr "Ajouter une session"
-
-#: proposals/templates/proposals/track_list.html:21
-msgid "manager"
-msgstr "responsable"
-
-#: proposals/templates/proposals/track_list.html:33
-msgid "No tracks."
-msgstr "Aucune session."
-
-#: proposals/views.py:203
+#: proposals/views.py:213
#, python-format
msgid "Note: the room %(room)s has %(capacity)s seat."
msgid_plural "Note: the room %(room)s has %(capacity)s seats."
msgstr[0] "Note : la salle %(room)s a %(capacity)s place."
msgstr[1] "Note : la salle %(room)s a %(capacity)s places."
-#: proposals/views.py:214
+#: proposals/views.py:225
msgid "Talk modified successfully!"
msgstr "Exposé modifié avec succès !"
-#: proposals/views.py:220
+#: proposals/views.py:231
msgid "Talk proposed successfully!"
msgstr "Exposé proposé avec succès !"
-#: proposals/views.py:237
+#: proposals/views.py:248
msgid "Talk assigned to track successfully!"
msgstr "Exposé assigné à la session avec succès !"
-#: proposals/views.py:310
-msgid "Vote successfully created"
-msgstr "A voté !"
-
-#: proposals/views.py:310
-msgid "Vote successfully updated"
-msgstr "Vote mis à jour"
-
-#: proposals/views.py:336
-msgid "Decision taken in account"
-msgstr "Décision enregistrée"
-
-#: proposals/views.py:434
+#: proposals/views.py:445
msgid "Unregistered :-("
msgstr "Vous avez été désinscrit :-("
-#: proposals/views.py:436
+#: proposals/views.py:447
msgid "Already registered!"
msgstr "Vous êtes déjà inscrit !"
-#: proposals/views.py:441
+#: proposals/views.py:452
msgid "Registered!"
msgstr "Vous avez été inscrit !"
@@ -1044,6 +1433,30 @@ msgstr ""
"Nous ne sommes pas encore en recherche de bénévoles … mais revenez plus "
"tard !"
+#: volunteers/templates/volunteers/volunteer_list.html:9
+msgid "Volunteers"
+msgstr "Bénévoles"
+
#: volunteers/templates/volunteers/volunteer_list.html:25
msgid "volunteer"
msgstr "bénévole"
+
+#~ msgid "Send a message"
+#~ msgstr "Envoyer un message"
+
+#~ msgid "Conversation with %(correspondent)s"
+#~ msgstr "Conversation avec %(correspondent)s"
+
+#~ msgid "Correspondents"
+#~ msgstr "Correspondants"
+
+#~ msgid "This is the list of participants that you follow."
+#~ msgstr "Ceci est la liste des participants que vous suivez."
+
+#~ msgid "You can use this page to communicate with the staff."
+#~ msgstr ""
+#~ "Vous pouvez utiliser cette page pour communiquer avec l’équipe "
+#~ "organisatrice."
+
+#~ msgid "Your talk \"{}\" has been submitted for {}"
+#~ msgstr "Votre proposition \"{}\" a été transmise à {}"
diff --git a/conversations/migrations/__init__.py b/mailing/__init__.py
similarity index 100%
rename from conversations/migrations/__init__.py
rename to mailing/__init__.py
diff --git a/mailing/admin.py b/mailing/admin.py
new file mode 100644
index 0000000..3a95dfb
--- /dev/null
+++ b/mailing/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+
+from .models import Message
+
+
+admin.site.register(Message)
diff --git a/conversations/forms.py b/mailing/forms.py
similarity index 97%
rename from conversations/forms.py
rename to mailing/forms.py
index f84879c..80eab2f 100644
--- a/conversations/forms.py
+++ b/mailing/forms.py
@@ -1,5 +1,6 @@
from django.forms.models import modelform_factory
-
+
from .models import Message
+
MessageForm = modelform_factory(Message, fields=['content'])
diff --git a/proposals/migrations/__init__.py b/mailing/management/__init__.py
similarity index 100%
rename from proposals/migrations/__init__.py
rename to mailing/management/__init__.py
diff --git a/mailing/management/commands/__init__.py b/mailing/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mailing/management/commands/fetchmail.py b/mailing/management/commands/fetchmail.py
new file mode 100644
index 0000000..180183e
--- /dev/null
+++ b/mailing/management/commands/fetchmail.py
@@ -0,0 +1,34 @@
+from django.core.management.base import BaseCommand
+
+from mailing.utils import fetch_imap_box
+
+
+class Command(BaseCommand):
+ help = 'Fetch emails from IMAP inbox'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--host', required=True)
+ parser.add_argument('--port', type=int)
+ parser.add_argument('--user', required=True)
+ parser.add_argument('--password', required=True)
+ parser.add_argument('--inbox')
+ grp = parser.add_mutually_exclusive_group()
+ grp.add_argument('--trash')
+ grp.add_argument('--no-trash', action='store_true')
+
+
+ def handle(self, *args, **options):
+ params = {
+ 'host': options['host'],
+ 'user': options['user'],
+ 'password': options['password'],
+ }
+ if options['port']:
+ params['port'] = options['port']
+ if options['inbox']:
+ params['inbox'] = options['inbox']
+ if options['trash']:
+ params['trash'] = options['trash']
+ elif options['no_trash']:
+ params['trash'] = None
+ fetch_imap_box(**params)
diff --git a/mailing/migrations/0001_initial.py b/mailing/migrations/0001_initial.py
new file mode 100644
index 0000000..2155da5
--- /dev/null
+++ b/mailing/migrations/0001_initial.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-01 15:48
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import mailing.models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('author', models.EmailField(blank=True, max_length=254)),
+ ('content', models.TextField(blank=True)),
+ ('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
+ ],
+ options={
+ 'ordering': ['created'],
+ },
+ ),
+ migrations.CreateModel(
+ name='MessageCorrespondent',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('email', models.EmailField(max_length=254)),
+ ('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='MessageThread',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('token', models.CharField(default=mailing.models.generate_message_token, max_length=64, unique=True)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='message',
+ name='thread',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailing.MessageThread'),
+ ),
+ ]
diff --git a/mailing/migrations/0002_message_author.py b/mailing/migrations/0002_message_author.py
new file mode 100644
index 0000000..05045a9
--- /dev/null
+++ b/mailing/migrations/0002_message_author.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.3 on 2017-08-02 09:27
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('mailing', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='message',
+ old_name='author',
+ new_name='from_email',
+ ),
+ migrations.AlterField(
+ model_name='message',
+ name='from_email',
+ field=models.EmailField(max_length=254),
+ ),
+ migrations.AddField(
+ model_name='message',
+ name='author_id',
+ field=models.PositiveIntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='message',
+ name='author_type',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
+ ),
+ ]
diff --git a/mailing/migrations/__init__.py b/mailing/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mailing/models.py b/mailing/models.py
new file mode 100644
index 0000000..d6fa09c
--- /dev/null
+++ b/mailing/models.py
@@ -0,0 +1,103 @@
+from django.db import models
+from django.utils.crypto import get_random_string
+from django.core.mail import EmailMessage, get_connection
+from django.conf import settings
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth import get_user_model
+
+import hashlib
+
+
+def generate_message_token():
+ # /!\ birthday problem
+ return get_random_string(length=32)
+
+
+def hexdigest_sha256(*args):
+ r = hashlib.sha256()
+ for arg in args:
+ r.update(str(arg).encode('utf-8'))
+ return r.hexdigest()
+
+
+class MessageCorrespondent(models.Model):
+ email = models.EmailField()
+ token = models.CharField(max_length=64, default=generate_message_token, unique=True)
+
+
+class MessageThread(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ token = models.CharField(max_length=64, default=generate_message_token, unique=True)
+
+
+class MessageManager(models.Manager):
+ def get_queyset(self):
+ qs = super().get_queryset()
+ # Does not work so well as prefetch_related is limited to one content type for generic foreign keys
+ qs = qs.prefetch_related('author')
+ return qs
+
+
+class Message(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ thread = models.ForeignKey(MessageThread)
+ author_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
+ author_id = models.PositiveIntegerField(null=True, blank=True)
+ author = GenericForeignKey('author_type', 'author_id')
+ from_email = models.EmailField()
+ content = models.TextField(blank=True)
+ token = models.CharField(max_length=64, default=generate_message_token, unique=True)
+
+ objects = MessageManager()
+
+ class Meta:
+ ordering = ['created']
+
+ def send_notification(self, subject, sender, dests, reply_to=None, message_id=None, reference=None, footer=None):
+ messages = []
+ for dest_name, dest_email in dests:
+ correspondent, created = MessageCorrespondent.objects.get_or_create(email=dest_email)
+ token = self.thread.token + correspondent.token + hexdigest_sha256(settings.SECRET_KEY, self.thread.token, correspondent.token)[:16]
+ if reply_to:
+ reply_to_name, reply_to_email = reply_to
+ reply_to_list = ['%s <%s>' % (reply_to_name, reply_to_email.format(token=token))]
+ else:
+ reply_to_list = []
+ headers = dict()
+ if message_id:
+ headers.update({
+ 'Message-ID': message_id.format(id=self.token),
+ })
+ if message_id and reference:
+ headers.update({
+ 'References': message_id.format(id=reference),
+ })
+ body = self.content
+ if footer is not None:
+ body += footer
+ messages.append(EmailMessage(
+ subject=subject,
+ body=body,
+ from_email='%s <%s>' % sender,
+ to=['%s <%s>' % (dest_name, dest_email)],
+ reply_to=reply_to_list,
+ headers=headers,
+ ))
+ connection = get_connection()
+ connection.send_messages(messages)
+
+ @property
+ def author_display(self):
+ if self.author:
+ author_class = ContentType.objects.get_for_model(self.author).model_class()
+ if author_class == get_user_model():
+ return self.author.get_full_name()
+ else:
+ return str(self.author)
+ else:
+ return self.from_email
+
+ def __str__(self):
+ return _("Message from %(author)s") % {'author': self.author_display}
diff --git a/conversations/templates/conversations/_message_form.html b/mailing/templates/mailing/_message_form.html
similarity index 81%
rename from conversations/templates/conversations/_message_form.html
rename to mailing/templates/mailing/_message_form.html
index 7ef8e91..f03d90b 100644
--- a/conversations/templates/conversations/_message_form.html
+++ b/mailing/templates/mailing/_message_form.html
@@ -1,10 +1,11 @@
{% load i18n %}
+
- {% trans "Send a message" %}
+ {{ message_form_title }}
-
+
{% csrf_token %}
diff --git a/mailing/templates/mailing/_message_list.html b/mailing/templates/mailing/_message_list.html
new file mode 100644
index 0000000..f79d128
--- /dev/null
+++ b/mailing/templates/mailing/_message_list.html
@@ -0,0 +1,14 @@
+{% load i18n %}
+
+{% for message in messages %}
+
+
+ {{ message.created }} | {{ message.author_display }}
+
+
+ {{ message.content|linebreaksbr }}
+
+
+{% empty %}
+
{% trans "No messages." %}
+{% endfor %}
diff --git a/mailing/tests.py b/mailing/tests.py
new file mode 100644
index 0000000..5b67ee3
--- /dev/null
+++ b/mailing/tests.py
@@ -0,0 +1,93 @@
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse
+from django.test import TestCase, override_settings
+from django.core import mail
+from django.conf import settings
+
+from .models import Message, MessageThread, MessageCorrespondent
+
+
+#class MailingTests(TestCase):
+# def setUp(self):
+# a, b, c, d = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abcd')
+# d.is_superuser = True
+# d.save()
+# pa, _ = Participation.objects.get_or_create(user=a, site=Site.objects.first())
+# conversation, _ = ConversationWithParticipant.objects.get_or_create(participation=pa)
+# Message.objects.create(content='allo', conversation=conversation, author=b)
+# Message.objects.create(content='aluil', conversation=conversation, author=a)
+# site = Site.objects.first()
+# Talk.objects.get_or_create(site=site, proposer=a, title='a talk', description='yay', event=Event.objects.get(site=site, name='other'))
+#
+# def test_models(self):
+# talk, participant, message = (model.objects.first() for model in
+# (ConversationAboutTalk, ConversationWithParticipant, Message))
+# self.assertEqual(str(talk), 'Conversation about a talk')
+# self.assertEqual(str(participant), 'Conversation with a')
+# self.assertEqual(str(message), 'Message from b')
+# self.assertEqual(message.get_absolute_url(), '/conversations/with/a/')
+# self.assertEqual(talk.get_absolute_url(), '/talk/details/a-talk')
+#
+# def test_views(self):
+# url = ConversationWithParticipant.objects.first().get_absolute_url()
+# self.assertEqual(self.client.get(url).status_code, 302)
+# self.client.login(username='c', password='c')
+# self.assertEqual(self.client.get(url).status_code, 403)
+# self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 403) # c is not staff
+# self.assertEqual(self.client.get(reverse('inbox')).status_code, 200)
+# self.client.post(reverse('inbox'), {'content': 'coucou'})
+# self.client.login(username='d', password='d')
+# self.client.post(url, {'content': 'im superuser'})
+# self.assertEqual(Message.objects.last().content, 'im superuser')
+# self.client.login(username='d', password='d')
+# self.assertEqual(self.client.get(reverse('list-correspondents')).status_code, 200)
+#
+#
+#@override_settings(DEFAULT_FROM_EMAIL='noreply@example.org',
+# REPLY_EMAIL='reply@example.org',
+# REPLY_KEY='secret')
+#class EmailTests(TestCase):
+# def setUp(self):
+# for guy in 'abcd':
+# setattr(self, guy, User.objects.create_user(guy, email='%s@example.org' % guy, password=guy))
+# a_p = Participation(user=self.a, site=Site.objects.first())
+# a_p.orga = True
+# a_p.save()
+# t = Topic(name='Topic 1', site=Site.objects.first())
+# t.save()
+# t.reviewers.add(self.b)
+#
+#
+# def test_talk_notification(self):
+# self.client.login(username='c', password='c')
+# # Check that login create participation
+# self.assertTrue(Participation.objects.filter(user=self.c, site=Site.objects.first()).exists())
+# # Propose new talk
+# topic = Topic.objects.get(name='Topic 1')
+# response = self.client.post(reverse('add-talk'), {
+# 'title': 'Talk 1',
+# 'description': 'This is the first talk',
+# 'topics': (topic.pk,),
+# 'event': 1,
+# 'speakers': (self.c.pk, self.d.pk),
+# }, follow=True)
+# self.assertEqual(response.status_code, 200)
+# self.assertContains(response, 'Talk proposed') # check messages notification
+# talk = Talk.objects.get(site=Site.objects.first(), title='Talk 1')
+# conv = ConversationAboutTalk.objects.get(talk=talk)
+# # Orga and reviewer should have been subscribed to the conversation about the talk
+# self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all()))
+# # Both should have received an email notification
+# self.assertEqual(len(mail.outbox), 2)
+# for m in mail.outbox:
+# self.assertEqual(m.from_email, '%s <%s>' % (self.c.profile, settings.DEFAULT_FROM_EMAIL))
+# self.assertTrue('Talk: %s' % talk.title)
+# self.assertTrue(len(m.to), 1)
+# self.assertTrue(m.to[0] in [ self.a.email, self.b.email ])
+# # Both should have been subscribed to conversations with each speakers
+# for user in [self.c, self.d]:
+# # Participation should have been created as the user is a speaker
+# p = Participation.objects.get(user=user, site=Site.objects.first())
+# conv = ConversationWithParticipant.objects.get(participation=p)
+# self.assertEqual(set([self.a, self.b]), set(conv.subscribers.all()))
diff --git a/mailing/utils.py b/mailing/utils.py
new file mode 100644
index 0000000..a56e1f6
--- /dev/null
+++ b/mailing/utils.py
@@ -0,0 +1,122 @@
+from django.conf import settings
+
+import imaplib
+import ssl
+import logging
+from email import policy
+from email.parser import BytesParser
+import chardet
+import re
+
+from .models import MessageThread, MessageCorrespondent, Message, hexdigest_sha256
+
+
+class NoTokenFoundException(Exception):
+ pass
+
+class InvalidTokenException(Exception):
+ pass
+
+class InvalidKeyException(Exception):
+ pass
+
+
+def fetch_imap_box(user, password, host, port=993, inbox='INBOX', trash='Trash'):
+ logging.basicConfig(level=logging.DEBUG)
+ context = ssl.create_default_context()
+ success, failure = 0, 0
+ with imaplib.IMAP4_SSL(host=host, port=port, ssl_context=context) as M:
+ typ, data = M.login(user, password)
+ if typ != 'OK':
+ raise Exception(data[0].decode('utf-8'))
+ typ, data = M.enable('UTF8=ACCEPT')
+ if typ != 'OK':
+ raise Exception(data[0].decode('utf-8'))
+ if trash is not None:
+ # Vérification de l’existence de la poubelle
+ typ, data = M.select(mailbox=trash)
+ if typ != 'OK':
+ raise Exception(data[0].decode('utf-8'))
+ typ, data = M.select(mailbox=inbox)
+ if typ != 'OK':
+ raise Exception(data[0].decode('utf-8'))
+ typ, data = M.uid('search', None, 'UNSEEN')
+ if typ != 'OK':
+ raise Exception(data[0].decode('utf-8'))
+ for num in data[0].split():
+ typ, data = M.uid('fetch', num, '(RFC822)')
+ if typ != 'OK':
+ failure += 1
+ logging.warning(data[0].decode('utf-8'))
+ continue
+ raw_email = data[0][1]
+ try:
+ process_email(raw_email)
+ except Exception as e:
+ failure += 1
+ logging.exception("An error occured during mail processing")
+ if type(e) == NoTokenFoundException:
+ tag = 'NoTokenFound'
+ if type(e) == InvalidTokenException:
+ tag = 'InvalidToken'
+ if type(e) == InvalidKeyException:
+ tag = 'InvalidKey'
+ else:
+ tag = 'UnknowError'
+ typ, data = M.uid('store', num, '+FLAGS', tag)
+ if typ != 'OK':
+ logging.warning(data[0].decode('utf-8'))
+ continue
+ if trash is not None:
+ typ, data = M.uid('copy', num, trash)
+ if typ != 'OK':
+ failure += 1
+ logging.warning(data[0].decode('utf-8'))
+ continue
+ typ, data = M.uid('store', num, '+FLAGS', '\Deleted')
+ if typ != 'OK':
+ failure += 1
+ logging.warning(data[0].decode('utf-8'))
+ continue
+ success += 1
+ typ, data = M.expunge()
+ if typ != 'OK':
+ failure += 1
+ raise Exception(data[0].decode('utf-8'))
+ if failure:
+ total = success + failure
+ logging.info("Total: %d, success: %d, failure: %d" % (total, success, failure))
+
+
+def process_email(raw_email):
+ msg = BytesParser(policy=policy.default).parsebytes(raw_email)
+ body = msg.get_body(preferencelist=['plain'])
+ content = body.get_payload(decode=True)
+
+ charset = body.get_content_charset()
+ if not charset:
+ charset = chardet.detect(content)['encoding']
+ content = content.decode(charset)
+
+ regex = re.compile('^[^+@]+\+(?P
[a-zA-Z0-9]{80})@[^@]+$')
+
+ for addr in msg.get('To', '').split(','):
+ m = regex.match(addr.strip())
+ if m:
+ break
+
+ if not m:
+ raise NoTokenFoundException
+
+ token = m.group('token')
+ key = token[64:]
+ try:
+ thread = MessageThread.objects.get(token=token[:32])
+ sender = MessageCorrespondent.objects.get(token=token[32:64])
+ except models.DoesNotExist:
+ raise InvalidTokenException
+
+ if key != hexdigest_sha256(settings.SECRET_KEY, thread.token, sender.token)[:16]:
+ raise InvalidKeyException
+
+ Message.objects.create(thread=thread, from_email=sender.email, content=content)
diff --git a/planning/forms.py b/planning/forms.py
index 3f4c12f..95443d0 100644
--- a/planning/forms.py
+++ b/planning/forms.py
@@ -4,16 +4,17 @@ from .models import Room
class RoomForm(forms.ModelForm):
+ class Meta:
+ model = Room
+ fields = ['name', 'label', 'capacity']
+
def __init__(self, *args, **kwargs):
self.site = kwargs.pop('site')
super().__init__(*args, **kwargs)
- class Meta:
- model = Room
- fields = ['name', 'label', 'capacity', 'sound']
-
def clean_name(self):
name = self.cleaned_data['name']
- if self.instance and name != self.instance.name and Room.objects.filter(site=self.site, name=name).exists():
+ if (not self.instance or self.instance.name != name) \
+ and Room.objects.filter(site=self.site, name=name).exists():
raise self.instance.unique_error_message(self._meta.model, ['name'])
return name
diff --git a/planning/migrations/0002_room_sound.py b/planning/migrations/0002_room_sound.py
deleted file mode 100644
index c6106dd..0000000
--- a/planning/migrations/0002_room_sound.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.10.7 on 2017-05-26 11:18
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('planning', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='room',
- name='sound',
- field=models.BooleanField(default=False),
- ),
- ]
diff --git a/planning/models.py b/planning/models.py
index 4cebb46..4454805 100644
--- a/planning/models.py
+++ b/planning/models.py
@@ -14,7 +14,6 @@ class Room(models.Model):
name = models.CharField(max_length=256, blank=True, default="")
label = models.CharField(max_length=256, blank=True, default="")
capacity = models.IntegerField(default=0)
- sound = models.BooleanField(default=False)
class Meta:
unique_together = ['site', 'name']
diff --git a/ponyconf/backends.py b/ponyconf/backends.py
new file mode 100644
index 0000000..29d3dea
--- /dev/null
+++ b/ponyconf/backends.py
@@ -0,0 +1,16 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.backends import ModelBackend
+
+
+class EmailBackend(ModelBackend):
+ def authenticate(self, request, username=None, password=None, **kwargs):
+ if not username:
+ return None
+ UserModel = get_user_model()
+ try:
+ user = UserModel._default_manager.get(email__iexact=username)
+ except UserModel.DoesNotExist:
+ UserModel().set_password(password) # https://code.djangoproject.com/ticket/20760
+ else:
+ if user.check_password(password) and self.user_can_authenticate(user):
+ return user
diff --git a/ponyconf/settings.py b/ponyconf/settings.py
index f2fa409..7f34d22 100644
--- a/ponyconf/settings.py
+++ b/ponyconf/settings.py
@@ -37,19 +37,18 @@ INSTALLED_APPS = [
'django.contrib.sites',
# our apps
- 'accounts',
+ #'accounts',
'ponyconf',
- 'proposals',
- 'conversations',
- 'planning',
- 'volunteers',
+ 'cfp',
+ 'mailing',
+ #'planning',
+ #'volunteers',
# external apps
'djangobower',
'bootstrap3',
- 'registration',
'django_select2',
- 'avatar',
+ 'crispy_forms',
# build-in apps
'django.contrib.admin',
@@ -60,7 +59,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
]
-MIDDLEWARE_CLASSES = [
+MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
@@ -70,6 +69,8 @@ MIDDLEWARE_CLASSES = [
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+
+ 'cfp.middleware.ConferenceMiddleware',
]
ROOT_URLCONF = 'ponyconf.urls'
@@ -86,8 +87,8 @@ TEMPLATES = [
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
- 'ponyconf.context_processors.site',
- 'proposals.context_processors.conference',
+ #'ponyconf.context_processors.site',
+ 'cfp.context_processors.conference',
],
},
},
@@ -204,10 +205,16 @@ BOOTSTRAP3 = {
SELECT2_JS = 'select2/dist/js/select2.min.js'
SELECT2_CSS = 'select2/dist/css/select2.min.css'
+SELECT2_I18N_PATH = 'select2/dist/js/i18n'
-AUTHENTICATION_BACKENDS = ['yeouia.backends.YummyEmailOrUsernameInsensitiveAuth']
+AUTHENTICATION_BACKENDS = [
+ 'django.contrib.auth.backends.ModelBackend',
+ 'ponyconf.backends.EmailBackend',
+]
LOGOUT_REDIRECT_URL = 'home'
+CRISPY_TEMPLATE_PACK='bootstrap3'
+
# django-registration
ACCOUNT_ACTIVATION_DAYS = 7
INCLUDE_REGISTER_URL = True
@@ -215,5 +222,17 @@ INCLUDE_REGISTER_URL = True
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
- }
+ },
+ 'select2': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': 'select2',
+ },
}
+
+SELECT2_CACHE_BACKEND = 'select2'
+
+SERVER_EMAIL = 'ponyconf@example.com'
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = 'localhost'
+EMAIL_PORT = 25
+
diff --git a/ponyconf/static/css/ponyconf.css b/ponyconf/static/css/ponyconf.css
index 6aa60ca..c1e4afc 100644
--- a/ponyconf/static/css/ponyconf.css
+++ b/ponyconf/static/css/ponyconf.css
@@ -1,3 +1,9 @@
.subnav {
padding-bottom: 20px;
}
+
+span.asteriskField {
+ color: red;
+ font-size: 15px;
+}
+
diff --git a/ponyconf/templates/_base.html b/ponyconf/templates/_base.html
index fa47fb3..091d020 100644
--- a/ponyconf/templates/_base.html
+++ b/ponyconf/templates/_base.html
@@ -8,7 +8,7 @@
{% comment %} {% endcomment %}
- {% block title %}{{ site.name }}{% endblock %}
+ {% block title %}{{ conference.name }}{% endblock %}
{% bootstrap_css %}
@@ -16,6 +16,10 @@
{% block js %}{% endblock %}
+ {% if conference.external_css_link %}
+
+ {% endif %}
+