Merge branch 'v2'
This commit is contained in:
commit
8a8474cd8c
|
@ -1 +0,0 @@
|
||||||
default_app_config = 'accounts.apps.AccountsConfig'
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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')
|
|
|
@ -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')]),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -1,76 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load accounts_tags i18n avatar_tags %}
|
|
||||||
|
|
||||||
{% block participantstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{{ profile }}</h1>
|
|
||||||
|
|
||||||
{% if request|staff %}
|
|
||||||
<a href="{% url 'user-conversation' profile.user.username %}" class="btn btn-primary ">{% trans "Contact" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if request|edit_profile:profile %}
|
|
||||||
<a href="{% url 'edit-participant' profile.user.username %}" class="btn btn-success">{% trans "Edit" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<img class="pull-right" src="{% avatar_url profile.user 160 %}" />
|
|
||||||
|
|
||||||
<h2>{% trans "Biography" %}</h2>
|
|
||||||
<p>{{ profile.biography }}</p>
|
|
||||||
|
|
||||||
{% if request|staff %}
|
|
||||||
<p><a href="mailto:{{ profile.user.email }}?subject=[Capitole du Libre]">{{ profile.user.email }}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h2>{% trans "Talks" %}</h2>
|
|
||||||
{% include "proposals/_talk_list.html" %}
|
|
||||||
|
|
||||||
{% if request|edit_profile:profile %}
|
|
||||||
|
|
||||||
<h2>{% trans "Information" %}</h2>
|
|
||||||
|
|
||||||
<h3>{% trans "Travels and hosting" %}</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<b>{% trans "Need transport:" %}</b> {{ participation.need_transport|yesno:"Yes,No,Not specified" }}
|
|
||||||
{% if participation.need_transport %}
|
|
||||||
<ul>
|
|
||||||
{% comment %}
|
|
||||||
<li><b>{% trans "Arrival:" %}</b> {{ participation.arrival }}</li>
|
|
||||||
<li><b>{% trans "Departure:" %}</b> {{ participation.departure }}</li>
|
|
||||||
{% endcomment %}
|
|
||||||
<li><b>{% trans "Accepted transport means:" %}</b> {% for transport in participation.transport.all %}{% if not forloop.first %}, {% endif %}{{ transport }}{% endfor %}</li>
|
|
||||||
<li><b>{% trans "Departure city:" %}</b> {{ participation.transport_city_outward|default:'Not specified' }}</li>
|
|
||||||
{% if participation.transport_city_return %}
|
|
||||||
<li><b>{% trans "Return city:" %}</b> {{ participation.transport_city_return }}</li>
|
|
||||||
{% endif %}
|
|
||||||
<li><b>{% trans "Transport booked:" %}</b> {{ participation.transport_booked|yesno }}</li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
<li><b>{% trans "Need accommodation:" %}</b> {% if participation.accommodation is None %}Not specified{% else %}{{ participation.get_accommodation_display }}{% endif %}</li>
|
|
||||||
<li><b>{% trans "Accommodation booked:" %}</b> {{ participation.accommodation_booked|yesno }}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>{% trans "Talk needs" %}</h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><b>{% trans "Video output:" %}</b> {% for conn in participation.connector.all %}{% if not forloop.first %}, {% endif %}{{ conn }}{% endfor %}</li>
|
|
||||||
<li><b>{% trans "Need sound:" %}</b> {{ participation.sound|yesno }}</li>
|
|
||||||
<li><b>{% trans "Ok to be recorded on video:" %}</b> {{ participation.videotaped|yesno }}</li>
|
|
||||||
<li><b>{% trans "Video licence:" %}</b> {{ participation.get_video_licence_display }}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>{% trans "Constraints" %}</h3>
|
|
||||||
<p>{{ participation.constraints|linebreaksbr }}</p>
|
|
||||||
|
|
||||||
<h3>{% trans "Notes" %}</h3>
|
|
||||||
<p>{{ participation.notes|linebreaksbr }}</p>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block participantstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3>{% blocktrans %}{{ profile }}'s profile{% endblocktrans %}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% include "_form.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,62 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 accounts_tags i18n %}
|
|
||||||
|
|
||||||
{% block participantstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% trans "Participants" %}</h1>
|
|
||||||
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Full name</th>
|
|
||||||
<th>Orga</th>
|
|
||||||
<th>Reviews</th>
|
|
||||||
<th>Conversations</th>
|
|
||||||
</tr>
|
|
||||||
{% for participation in participation_list %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{{ participation.get_absolute_url }}">{{ participation.user.username }}</a></td>
|
|
||||||
<td>{{ participation.user.get_full_name }}</td>
|
|
||||||
<td>{{ participation.is_orga|yesno:"✔,✘" }}</td>
|
|
||||||
<td>{% for topic in participation.topic_set.all %}{{ topic.get_link }}{% if not forloop.last %},
|
|
||||||
{% endif %}{% endfor %}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'user-conversation' participation.user.username %}" data-toggle="tooltip" data-placement="bottom"
|
|
||||||
title="{% trans "View conversation" %}"><span class="glyphicon glyphicon-envelope"></span></a>
|
|
||||||
{% if request.user in participation.conversation.subscribers.all %}
|
|
||||||
<a href="{% url 'unsubscribe-conversation' participation.user.username %}?next={% url 'list-participants' %}"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="{% trans "Unsubscribe from the conversation" %}">
|
|
||||||
<span class="glyphicon glyphicon-star"></span></a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'subscribe-conversation' participation.user.username %}?next={% url 'list-participants' %}"
|
|
||||||
data-toggle="tooltip" data-placement="bottom" title="{% trans "Subscribe to the conversation" %}">
|
|
||||||
<span class="glyphicon glyphicon-star-empty"></span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% if request|orga %}
|
|
||||||
{% include "_form.html" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ block.css }}
|
|
||||||
{{ form.media.css }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_end %}
|
|
||||||
{{ block.super }}
|
|
||||||
{{ form.media.js }}
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,65 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n avatar_tags %}
|
|
||||||
|
|
||||||
{% block profiletab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3>{% trans "Profile" %}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<form action="" method="post" class="form-horizontal">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-md-3 control-label" for="id_first_name">Avatar</label>
|
|
||||||
{% avatar request.user %}
|
|
||||||
<a href="{% url 'avatar_change' %}" class="btn btn-default">{% trans "Change avatar" %}</a>
|
|
||||||
</div>
|
|
||||||
{% bootstrap_form user_form layout="horizontal" %}
|
|
||||||
{% bootstrap_form profile_form layout="horizontal" %}
|
|
||||||
{% bootstrap_field participation_form.need_transport layout="horizontal" %}
|
|
||||||
<div id="transport_fields">
|
|
||||||
{% bootstrap_field participation_form.transport layout="horizontal" %}
|
|
||||||
{% bootstrap_field participation_form.transport_city_outward layout="horizontal" %}
|
|
||||||
{% bootstrap_field participation_form.transport_city_return layout="horizontal" %}
|
|
||||||
</div>
|
|
||||||
{% bootstrap_form participation_form exclude="need_transport,transport,transport_city_outward,transport_city_return" layout="horizontal" %}
|
|
||||||
{% buttons layout="horizontal" %}
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
|
||||||
{% for url, class, text in buttons %}
|
|
||||||
<a href="{% url url %}" class="btn btn-{{ class }}">{{ text }}</a>
|
|
||||||
{% endfor %}
|
|
||||||
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'home' %}{% endif %}" class="btn btn-default">{% trans "Cancel" %}</a>
|
|
||||||
{% endbuttons %}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ block.super }}
|
|
||||||
{{ form.media.css }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_end %}
|
|
||||||
{{ block.super }}
|
|
||||||
{{ form.media.js }}
|
|
||||||
<script type="text/javascript">
|
|
||||||
var update_transport = function() {
|
|
||||||
if ($('#id_need_transport').val() == 2) { // 2 == 'Yes'
|
|
||||||
$("#transport_fields").show();
|
|
||||||
} else {
|
|
||||||
$("#transport_fields").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$(function(){
|
|
||||||
update_transport();
|
|
||||||
$('#id_need_transport').change(update_transport);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
|
@ -1,14 +0,0 @@
|
||||||
{% extends "avatar/base.html" %}
|
|
||||||
{% load i18n avatar_tags %}
|
|
||||||
|
|
||||||
{% block avatarcontent %}
|
|
||||||
<p>{% trans "Your current avatar: " %}</p>
|
|
||||||
{% avatar user %}
|
|
||||||
{% if not avatars %}
|
|
||||||
<p>{% trans "You haven't uploaded an avatar yet. Please upload one now." %}</p>
|
|
||||||
{% endif %}
|
|
||||||
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar_add' %}">
|
|
||||||
{{ upload_avatar_form.as_p }}
|
|
||||||
<p>{% csrf_token %}<input type="submit" value="{% trans "Upload New Image" %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1 +0,0 @@
|
||||||
<img src="{{ url }}" alt="{{ alt }}" width="{{ size }}" height="{{ size }}" />
|
|
|
@ -1,20 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block profiletab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h3>{% trans "Avatar" %}
|
|
||||||
<a href="{% url 'profile' %}" class="btn btn-primary pull-right"><span class="glyphicon glyphicon-arrow-left"> {% trans "Back to profile" %}</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{% block avatarcontent %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{% extends "avatar/base.html" %}
|
|
||||||
{% load i18n avatar_tags %}
|
|
||||||
|
|
||||||
{% block avatarcontent %}
|
|
||||||
<p>{% trans "Your current avatar: " %}</p>
|
|
||||||
{% avatar user %}
|
|
||||||
{% if not avatars %}
|
|
||||||
<p>{% trans "You haven't uploaded an avatar yet. Please upload one now." %}</p>
|
|
||||||
{% else %}
|
|
||||||
<form method="POST" action="{% url 'avatar_change' %}">
|
|
||||||
<ul>
|
|
||||||
{{ primary_avatar_form.as_ul }}
|
|
||||||
</ul>
|
|
||||||
<p>{% csrf_token %}<input type="submit" value="{% trans "Choose new Default" %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<form enctype="multipart/form-data" method="POST" action="{% url 'avatar_add' %}">
|
|
||||||
{{ upload_avatar_form.as_p }}
|
|
||||||
<p>{% csrf_token %}<input type="submit" value="{% trans "Upload New Image" %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends "avatar/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block avatarcontent %}
|
|
||||||
<p>{% trans "Please select the avatars that you would like to delete." %}</p>
|
|
||||||
{% if not avatars %}
|
|
||||||
{% url 'avatar_change' as avatar_change_url %}
|
|
||||||
<p>{% blocktrans %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endblocktrans %}</p>
|
|
||||||
{% else %}
|
|
||||||
<form method="POST" action="{% url 'avatar_delete' %}">
|
|
||||||
<ul>
|
|
||||||
{{ delete_avatar_form.as_ul }}
|
|
||||||
</ul>
|
|
||||||
<p>{% csrf_token %}<input type="submit" value="{% trans "Delete These" %}" /></p>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,30 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
|
||||||
|
|
||||||
{% block logintab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>
|
|
||||||
{% trans "Login" %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-offset-3 col-md-6">
|
|
||||||
<div class="well">
|
|
||||||
{% include '_form.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-offset-4 col-md-4">
|
|
||||||
{% url 'registration_register' as reg_url %}
|
|
||||||
{% blocktrans %}You do not have an account yet? Please <a href="{{ reg_url }}">register</a>.{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>
|
|
||||||
{% trans "Password Reset" %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-offset-4 col-md-4">
|
|
||||||
<div class="well">
|
|
||||||
{% include "_form.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,30 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
|
||||||
|
|
||||||
{% block registrationtab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>
|
|
||||||
{% trans "Registration" %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-offset-3 col-md-6">
|
|
||||||
<div class="well">
|
|
||||||
{% include '_form.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-offset-4 col-md-4">
|
|
||||||
{% url 'login' as login_url %}
|
|
||||||
{% blocktrans %}You already have an account? Please <a href="{{ login_url }}">login</a>.{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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<username>[\w.@+-]+)$', views.participant_details, name='show-participant'),
|
|
||||||
url(r'^participant/(?P<username>[\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')),
|
|
||||||
]
|
|
|
@ -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()
|
|
|
@ -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),
|
|
||||||
})
|
|
1
cfp/__init__.py
Normal file
1
cfp/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'cfp.apps.CFPConfig'
|
43
cfp/admin.py
Normal file
43
cfp/admin.py
Normal file
|
@ -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)
|
10
cfp/apps.py
Normal file
10
cfp/apps.py
Normal file
|
@ -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)
|
4
cfp/context_processors.py
Normal file
4
cfp/context_processors.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
|
||||||
|
def conference(request):
|
||||||
|
return {'conference': request.conference}
|
|
@ -3,18 +3,7 @@ from functools import wraps
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from accounts.utils import is_orga, is_staff
|
from cfp.utils import 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)
|
|
||||||
|
|
||||||
|
|
||||||
def staff_required(view_func):
|
def staff_required(view_func):
|
191
cfp/forms.py
Normal file
191
cfp/forms.py
Normal file
|
@ -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']
|
16
cfp/middleware.py
Normal file
16
cfp/middleware.py
Normal file
|
@ -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
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import autoslug.fields
|
import autoslug.fields
|
||||||
|
@ -8,7 +8,7 @@ from django.conf import settings
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import proposals.models
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -16,39 +16,72 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('planning', '0001_initial'),
|
|
||||||
('sites', '0002_alter_domain_unique'),
|
('sites', '0002_alter_domain_unique'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name='Conference',
|
name='Conference',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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='')),
|
('home', models.TextField(blank=True, default='')),
|
||||||
('venue', models.TextField(blank=True, default='')),
|
('venue', models.TextField(blank=True, default='')),
|
||||||
('city', models.CharField(blank=True, default='', max_length=64)),
|
('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')),
|
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
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=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(max_length=64)),
|
('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')),
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
'verbose_name': 'category',
|
||||||
|
'verbose_name_plural': 'categories',
|
||||||
'ordering': ('pk',),
|
'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(
|
migrations.CreateModel(
|
||||||
name='Track',
|
name='Track',
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -114,7 +107,6 @@ class Migration(migrations.Migration):
|
||||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||||
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name')),
|
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name')),
|
||||||
('description', models.TextField(blank=True, verbose_name='Description')),
|
('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')),
|
('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)),
|
('created', models.DateTimeField(auto_now_add=True)),
|
||||||
('updated', models.DateTimeField(auto_now=True)),
|
('updated', models.DateTimeField(auto_now=True)),
|
||||||
('vote', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(-2), django.core.validators.MaxValueValidator(2)])),
|
('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)),
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='topic',
|
model_name='talk',
|
||||||
name='track',
|
name='category',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='proposals.Track', verbose_name='Destination track'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cfp.TalkCategory', verbose_name='Talk Category'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='talk',
|
model_name='talk',
|
||||||
name='topics',
|
name='site',
|
||||||
field=models.ManyToManyField(blank=True, help_text='The topics can not be changed after submission.', to='proposals.Topic', verbose_name='Topics'),
|
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(
|
migrations.AddField(
|
||||||
model_name='talk',
|
model_name='talk',
|
||||||
name='track',
|
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(
|
migrations.AlterUniqueTogether(
|
||||||
name='vote',
|
name='vote',
|
||||||
|
@ -153,11 +150,11 @@ class Migration(migrations.Migration):
|
||||||
unique_together=set([('site', 'name')]),
|
unique_together=set([('site', 'name')]),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='topic',
|
name='talkcategory',
|
||||||
unique_together=set([('site', 'name')]),
|
unique_together=set([('site', 'name')]),
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='event',
|
name='participant',
|
||||||
unique_together=set([('site', 'name')]),
|
unique_together=set([('site', 'email')]),
|
||||||
),
|
),
|
||||||
]
|
]
|
22
cfp/migrations/0002_conference_staff.py
Normal file
22
cfp/migrations/0002_conference_staff.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
46
cfp/migrations/0003_auto_20170801_1400.py
Normal file
46
cfp/migrations/0003_auto_20170801_1400.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
59
cfp/migrations/0004_auto_20170801_1408.py
Normal file
59
cfp/migrations/0004_auto_20170801_1408.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
20
cfp/migrations/0005_conference_reply_email.py
Normal file
20
cfp/migrations/0005_conference_reply_email.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
26
cfp/migrations/0006_auto_20170811_1457.py
Normal file
26
cfp/migrations/0006_auto_20170811_1457.py
Normal file
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
20
cfp/migrations/0007_conference_secure_domain.py
Normal file
20
cfp/migrations/0007_conference_secure_domain.py
Normal file
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import autoslug.fields
|
import autoslug.fields
|
||||||
|
@ -9,10 +9,9 @@ import django.db.models.deletion
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('sites', '0002_alter_domain_unique'),
|
('sites', '0002_alter_domain_unique'),
|
||||||
|
('cfp', '0007_conference_secure_domain'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -30,6 +29,21 @@ class Migration(migrations.Migration):
|
||||||
'ordering': ['name'],
|
'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(
|
migrations.AlterUniqueTogether(
|
||||||
name='room',
|
name='room',
|
||||||
unique_together=set([('site', 'name')]),
|
unique_together=set([('site', 'name')]),
|
13
cfp/mixins.py
Normal file
13
cfp/mixins.py
Normal file
|
@ -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)
|
347
cfp/models.py
Normal file
347
cfp/models.py
Normal file
|
@ -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()
|
110
cfp/signals.py
Normal file
110
cfp/signals.py
Normal file
|
@ -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()
|
15
cfp/templates/cfp/closed.html
Normal file
15
cfp/templates/cfp/closed.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block proposetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>
|
||||||
|
{% trans "Participate" %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>{% trans "Sorry, the Call for Participation is closed!" %}</h2>
|
||||||
|
|
||||||
|
{% endblock %}
|
27
cfp/templates/cfp/complete.html
Normal file
27
cfp/templates/cfp/complete.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% load ponyconf_tags i18n %}
|
||||||
|
|
||||||
|
{% block proposetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>
|
||||||
|
{% trans "Your proposition have been successfully submitted!" %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p>{% trans "Thanks for your proposal" %} {{ participant }} !</p>
|
||||||
|
<p>{% trans "You can at anytime:" %}
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "Edit your talk:" %} <a href="{% url 'talk-proposal-edit' talk.token participant.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-edit' talk.token participant.token %}</a></li>
|
||||||
|
<li>{% trans "Add an additionnal speaker:" %} <a href="{% url 'talk-proposal-speaker-add' talk.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-speaker-add' talk.token %}</a></li>
|
||||||
|
<li>{% trans "Edit your profile:" %} <a href="{% url 'talk-proposal-speaker-edit' talk.token participant.token %}">{% if request.is_secure %}https{% else %}http{% endif %}://{{ conference.site.domain }}{% url 'talk-proposal-speaker-edit' talk.token participant.token %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p>{% trans "An email has been sent to you with those URLs" %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,11 +1,11 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% load proposals_tags i18n %}
|
{% load ponyconf_tags i18n %}
|
||||||
|
|
||||||
{% block hometab %} class="active"{% endblock %}
|
{% block hometab %} class="active"{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% markdown site.conference.home %}
|
{% markdown conference.home %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
27
cfp/templates/cfp/propose.html
Normal file
27
cfp/templates/cfp/propose.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% load ponyconf_tags i18n %}
|
||||||
|
|
||||||
|
{% block proposetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>
|
||||||
|
{% trans "Participate" %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ participant_form|crispy }}
|
||||||
|
{{ talk_form|crispy }}
|
||||||
|
<div class="col-md-12 text-center">
|
||||||
|
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
26
cfp/templates/cfp/speaker.html
Normal file
26
cfp/templates/cfp/speaker.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% load ponyconf_tags i18n %}
|
||||||
|
|
||||||
|
{% block proposetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>
|
||||||
|
{% trans "Participate" %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<form method="POST" class="form-horizontal col-md-8 col-md-offset-2">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ participant_form|crispy }}
|
||||||
|
<div class="col-md-12 text-center">
|
||||||
|
<button type="submit" class="btn btn-primary text-center">{% trans "Save" %} <i class="fa fa-check"></i></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -6,16 +6,24 @@
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<ul class="nav nav-tabs nav-justified subnav">
|
<ul class="nav nav-tabs nav-justified subnav">
|
||||||
|
{% comment %}
|
||||||
<li{% block topicstab %}{% endblock %}><a href="{% url 'list-topics' %}"><span class="glyphicon glyphicon-tag"></span> {% trans "Topics" %}</a></li>
|
<li{% block topicstab %}{% endblock %}><a href="{% url 'list-topics' %}"><span class="glyphicon glyphicon-tag"></span> {% trans "Topics" %}</a></li>
|
||||||
<li{% block trackstab %}{% endblock %}><a href="{% url 'list-tracks' %}"><span class="glyphicon glyphicon-screenshot"></span> {% trans "Tracks" %}</a></li>
|
|
||||||
<li{% block talkstab %}{% endblock %}><a href="{% url 'list-talks' %}"><span class="glyphicon glyphicon-blackboard"></span> {% trans "Talks" %}</a></li>
|
|
||||||
<li{% block speakerstab %}{% endblock %}><a href="{% url 'list-speakers' %}"><span class="glyphicon glyphicon-bullhorn"></span> {% trans "Speakers" %}</a></li>
|
|
||||||
<li{% block volunteerstab %}{% endblock %}><a href="{% url 'list-volunteers' %}"><span class="glyphicon glyphicon-thumbs-up"></span> {% trans "Volunteers" %}</a></li>
|
<li{% block volunteerstab %}{% endblock %}><a href="{% url 'list-volunteers' %}"><span class="glyphicon glyphicon-thumbs-up"></span> {% trans "Volunteers" %}</a></li>
|
||||||
<li{% block roomstab %}{% endblock %}><a href="{% url 'list-rooms' %}"><span class="glyphicon glyphicon-tent"></span> {% trans "Rooms" %}</a></li>
|
|
||||||
<li{% block scheduletab %}{% endblock %}><a href="{% url 'show-schedule' %}"><span class="glyphicon glyphicon-calendar"></span> {% trans "Schedule" %}</a></li>
|
<li{% block scheduletab %}{% endblock %}><a href="{% url 'show-schedule' %}"><span class="glyphicon glyphicon-calendar"></span> {% trans "Schedule" %}</a></li>
|
||||||
<li{% block participantstab %}{% endblock %}><a href="{% url 'list-participants' %}"><span class="glyphicon glyphicon-user"></span> {% trans "Participants" %}</a></li>
|
|
||||||
<li{% block correspondentstab %}{% endblock %}><a href="{% url 'list-correspondents' %}"><span class="glyphicon glyphicon-envelope"></span> {% trans "Correspondents" %}</a></li>
|
<li{% block correspondentstab %}{% endblock %}><a href="{% url 'list-correspondents' %}"><span class="glyphicon glyphicon-envelope"></span> {% trans "Correspondents" %}</a></li>
|
||||||
<li{% block conferencetab %}{% endblock %}><a href="{% url 'edit-conference' %}"><span class="glyphicon glyphicon-cog"></span> {% trans "Conference" %}</a></li>
|
<li{% block conferencetab %}{% endblock %}><a href="{% url 'edit-conference' %}"><span class="glyphicon glyphicon-cog"></span> {% trans "Conference" %}</a></li>
|
||||||
|
{% endcomment %}
|
||||||
|
<li{% block talkstab %}{% endblock %}><a href="{% url 'talk-list' %}"><span class="glyphicon glyphicon-blackboard"></span> {% trans "Talks" %}</a></li>
|
||||||
|
<li{% block speakerstab %}{% endblock %}><a href="{% url 'participant-list' %}"><span class="glyphicon glyphicon-bullhorn"></span> {% trans "Speakers" %}</a></li>
|
||||||
|
<li{% block trackstab %}{% endblock %}><a href="{% url 'track-list' %}"><span class="glyphicon glyphicon-screenshot"></span> {% trans "Tracks" %}</a></li>
|
||||||
|
<li{% block roomstab %}{% endblock %}><a href="{% url 'room-list' %}"><span class="glyphicon glyphicon-tent"></span> {% trans "Rooms" %}</a></li>
|
||||||
|
<li{% block conferencetab %}{% endblock %}><a href="{% url 'conference' %}"><span class="glyphicon glyphicon-asterisk"></span> {% trans "Conference" %}</a></li>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-dashboard"></span> Django-Admin</a></li>
|
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-dashboard"></span> Django-Admin</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% trans "Please select a category." %}
|
||||||
|
{% endblock %}
|
27
cfp/templates/cfp/staff/conference.html
Normal file
27
cfp/templates/cfp/staff/conference.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block conferencetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{% trans "Conference" %}</h1>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<a href="{% url 'create-user' %}" class="btn btn-default">{% trans "Add a new user" %}</a>
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_end %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
17
cfp/templates/cfp/staff/create_user.html
Normal file
17
cfp/templates/cfp/staff/create_user.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block conferencetab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{% trans "Add a new user" %}</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
|
||||||
|
<a class="btn btn-default" href="{% url 'conference' %}">{% trans "Cancel" %}</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
62
cfp/templates/cfp/staff/participant_details.html
Normal file
62
cfp/templates/cfp/staff/participant_details.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block speakerstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ participant }}</h1>
|
||||||
|
|
||||||
|
<p><a class="btn btn-success" href="{% url 'participant-edit' participant.token %}">{% trans "Edit" %}</a></p>
|
||||||
|
|
||||||
|
<h2>{% trans "Biography" %}</h2>
|
||||||
|
<p>{{ participant.biography|linebreaksbr }}</p>
|
||||||
|
|
||||||
|
{% if participant.notes %}
|
||||||
|
<h2>{% trans "Notes" %}</h2>
|
||||||
|
<p>{{ participant.notes|linebreaksbr }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2>{% trans "Informations" %}</h2>
|
||||||
|
<ul>
|
||||||
|
<li><b>{% trans "E-mail:" %}</b> <a href="mailto:{{ participant.email }}">{{ participant.email }}</a></li>
|
||||||
|
{% if participant.twitter %}<li><b>{% trans "Twitter:" %}</b> <a href="{{ participant.twitter }}">{{ participant.twitter }}</a></li>{% endif %}
|
||||||
|
{% if participant.linkedin %}<li><b>{% trans "LinkedIn:" %}</b> <a href="{{ participant.linkedin }}">{{ participant.linkedin }}</a></li>{% endif %}
|
||||||
|
{% if participant.github %}<li><b>{% trans "Github:" %}</b> <a href="{{ participant.github }}">{{ participant.github }}</a></li>{% endif %}
|
||||||
|
{% if participant.website %}<li><b>{% trans "Website:" %}</b> <a href="{{ participant.website }}">{{ participant.website }}</a></li>{% endif %}
|
||||||
|
{% if participant.facebook %}<li><b>{% trans "Facebook:" %}</b> <a href="{{ participant.facebook }}">{{ participant.facebook }}</a></li>{% endif %}
|
||||||
|
{% if participant.mastodon %}<li><b>{% trans "Mastodon:" %}</b> <a href="{{ participant.mastodon }}">{{ participant.mastodon }}</a></li>{% endif %}
|
||||||
|
{% if participant.phone_number %}<li><b>{% trans "Phone number:" %}</b> {{ participant.phone_number }}</li>{% endif %}
|
||||||
|
{% if participant.language %}<li><b>{% trans "Language:" %}</b> {{ participant.language }}</li>{% endif %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>{% trans "Talks" %}</h2>
|
||||||
|
{% regroup participant.talk_set.all by category as category_list %}
|
||||||
|
{% for category in category_list %}
|
||||||
|
<h3>{{ category.list.0.category }}</h3>
|
||||||
|
<ul>{% for talk in category.list %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'talk-details' talk.token %}">{{ talk }}</a>
|
||||||
|
<i>{% trans "by" %}</i>
|
||||||
|
{% for p in talk.speakers.all %}
|
||||||
|
{% if p == participant %}{{ p }}{% else %}<a href="{% url 'participant-details' p.token %}">{{ p }}</a>{% endif %}
|
||||||
|
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if talk.track %}
|
||||||
|
<i>{% trans "in" %}</i>
|
||||||
|
{{ talk.track }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% empty %}{% trans "No talks" %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<h2>{% trans "Messaging" %}</h2>
|
||||||
|
|
||||||
|
{% include 'mailing/_message_list.html' with messages=participant.conversation.message_set.all %}
|
||||||
|
|
||||||
|
{% trans "Send a message – <em>this message will be received by this participant and all the staff team</em>" as message_form_title %}
|
||||||
|
{% include 'mailing/_message_form.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
13
cfp/templates/cfp/staff/participant_form.html
Normal file
13
cfp/templates/cfp/staff/participant_form.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block speakerstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{% trans "Edit a speaker" %}</h1>
|
||||||
|
|
||||||
|
{% url 'participant-details' participant.token as cancel_url %}
|
||||||
|
{% include '_form.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends 'staff.html' %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
{% load bootstrap3 i18n %}
|
||||||
|
|
||||||
{% block speakerstab %} class="active"{% endblock %}
|
{% block speakerstab %} class="active"{% endblock %}
|
||||||
|
@ -8,6 +7,7 @@
|
||||||
|
|
||||||
<h1>{% trans "Speakers" %}</h1>
|
<h1>{% trans "Speakers" %}</h1>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
<a class="btn btn-primary" role="button" data-toggle="collapse" href="#filter" aria-expanded="{{ show_filters|yesno:"true,false" }}" aria-controles="filter">{% trans "Show filtering options…" %}</a>
|
<a class="btn btn-primary" role="button" data-toggle="collapse" href="#filter" aria-expanded="{{ show_filters|yesno:"true,false" }}" aria-controles="filter">{% trans "Show filtering options…" %}</a>
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
@ -39,63 +39,43 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover">
|
||||||
<caption>{% trans "Total:" %} {{ speaker_list|length }} {% trans "speaker" %}{{ speaker_list|length|pluralize }}
|
<caption>{% trans "Total:" %} {{ participant_list|length }} {% trans "speaker" %}{{ participant_list|length|pluralize }}
|
||||||
</caption>
|
</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-center">{% trans "Username" %}</th>
|
<th class="text-center">{% trans "Name" %}</th>
|
||||||
<th class="text-center">{% trans "Fullname" %}</th>
|
|
||||||
<th class="text-center">{% trans "Talk count" %}</th>
|
<th class="text-center">{% trans "Talk count" %}</th>
|
||||||
<th class="text-center">{% blocktrans context "table column title" %}Need transport?{% endblocktrans %}</th>
|
{% comment %}<th class="text-center"></th>{% endcomment %}
|
||||||
<th class="text-center">{% blocktrans context "table column title" %}Need accommodation?{% endblocktrans %}</th>
|
|
||||||
<th class="text-center">{% trans "Need sound?" %}</th>
|
|
||||||
<th class="text-center"></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
{% comment %}
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7">{% trans "Contact:" %} <a href="{{ contact_link }}">{% trans "link" %}</a></td>
|
<td colspan="7">{% trans "Contact:" %} <a href="{{ contact_link }}">{% trans "link" %}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</tfoot>
|
||||||
{% for speaker in speaker_list %}
|
{% endcomment %}
|
||||||
|
{% for participant in participant_list %}
|
||||||
{% if forloop.first %}
|
{% if forloop.first %}
|
||||||
<tbody>
|
<tbody>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'show-participant' username=speaker.user.username %}">{{ speaker.user.username }}</a></td>
|
<td><a href="{% url 'participant-details' participant.token %}">{{ participant }}</a></td>
|
||||||
<td>{{ speaker.user.get_full_name }}</td>
|
<td>
|
||||||
<td class="text-right">{{ speaker.not_refused_talk_set.count }}{% if speaker.pending_talk_set.count %} ({{ speaker.pending_talk_set.count }} pending){% endif %}</td>
|
<span class="text-success">{% blocktrans count accepted=participant.accepted_talk_count %}accepted: {{ accepted }}{% plural %}accepted: {{ accepted }}{% endblocktrans %}</span>
|
||||||
{% if speaker.need_transport %}
|
—
|
||||||
<td class="{% if speaker.transport_booked %}success{% else %}warning{% endif %}">
|
<span class="text-warning">{% blocktrans count pending=participant.pending_talk_count %}pending: {{ pending }}{% plural %}pending: {{ pending }}{% endblocktrans %}</span>
|
||||||
{% for transport in speaker.transport.all %}
|
—
|
||||||
{% if not forloop.first %}, {% endif %}
|
<span class="text-danger">{% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %}</span>
|
||||||
{{ transport }}
|
|
||||||
{% empty %}
|
|
||||||
Yes
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
</td>
|
||||||
{% elif speaker.need_transport is None %}
|
{% comment %}
|
||||||
<td>?</td>
|
|
||||||
{% else %}
|
|
||||||
<td>No</td>
|
|
||||||
{% endif %}
|
|
||||||
<td{% if speaker.accommodation is not None and speaker.accommodation != speaker.ACCOMMODATION_NO %} class="{% if speaker.accommodation_booked %}success{% else %}warning{% endif %}"{% endif %}>
|
|
||||||
{% if speaker.accommodation is None %}
|
|
||||||
?
|
|
||||||
{% else %}
|
|
||||||
{{ speaker.get_accommodation_display }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% if speaker.sound %}
|
|
||||||
<td class="warning">Yes</td>
|
|
||||||
{% else %}
|
|
||||||
<td>No</td>
|
|
||||||
{% endif %}
|
|
||||||
<td>
|
<td>
|
||||||
<a class="btn btn-{% if speaker.conversation.messages.last.author == speaker.user %}primary{% else %}default{% endif %}" href="{% url 'user-conversation' speaker.user.username %}">{% trans "Contact" %}</a>
|
<a class="btn btn-{% if speaker.conversation.messages.last.author == speaker.user %}primary{% else %}default{% endif %}" href="{% url 'user-conversation' speaker.user.username %}">{% trans "Contact" %}</a>
|
||||||
</td>
|
</td>
|
||||||
|
{% endcomment %}
|
||||||
</tr>
|
</tr>
|
||||||
{% if forloop.last %}
|
{% if forloop.last %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -105,6 +85,7 @@
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
{% block js_end %}
|
{% block js_end %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
jQuery(document).ready(function($) {
|
jQuery(document).ready(function($) {
|
||||||
|
@ -115,3 +96,4 @@ jQuery(document).ready(function($) {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% endcomment %}
|
48
cfp/templates/cfp/staff/room_details.html
Normal file
48
cfp/templates/cfp/staff/room_details.html
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
|
{% load bootstrap3 cfp_tags i18n %}
|
||||||
|
|
||||||
|
{% block roomstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{{ room.name }}
|
||||||
|
<small>{{ room.label }}</small>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<h2>{% trans "Scheduled talks" %}</h2>
|
||||||
|
{% for talk in room.talks_by_date %}
|
||||||
|
{% if forloop.first %}<ul>{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ talk.get_absolute_url }}"><strong>{{ talk }}</strong></a>
|
||||||
|
{% for participant in talk.speakers.all %}
|
||||||
|
{% if forloop.first %} – <em>{% endif %}
|
||||||
|
<a href="{% url 'participant-details' participant.token %}">{{ participant }}</a>
|
||||||
|
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
|
||||||
|
{% if forloop.last %}</em>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
– <span>{{ talk.start_date }} – {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}</span>
|
||||||
|
</li>
|
||||||
|
{% if forloop.last %}</ul>{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<em>{% trans "No talks." %}</em>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<h3>{% trans "Unscheduled talks" %}</h3>
|
||||||
|
{% for talk in room.unscheduled_talks %}
|
||||||
|
{% if forloop.first %}<ul>{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ talk.get_absolute_url }}"><strong>{{ talk }}</strong></a>
|
||||||
|
{% for participant in talk.speakers.all %}
|
||||||
|
{% if forloop.first %} – <em>{% endif %}
|
||||||
|
<a href="{% url 'participant-details' participant.token %}">{{ participant }}</a>
|
||||||
|
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
|
||||||
|
{% if forloop.last %}</em>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% if forloop.last %}</ul>{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<em>{% trans "No talks." %}</em>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends 'staff.html' %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
{% load bootstrap3 i18n %}
|
||||||
|
|
||||||
{% block topicstab %} class="active"{% endblock %}
|
{% block roomstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>{% trans "Topic" %}</h1>
|
<h1>{% trans "Room" %}</h1>
|
||||||
|
|
||||||
{% include "_form.html" %}
|
{% include "_form.html" %}
|
||||||
|
|
46
cfp/templates/cfp/staff/room_list.html
Normal file
46
cfp/templates/cfp/staff/room_list.html
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
|
{% load bootstrap3 cfp_tags i18n %}
|
||||||
|
|
||||||
|
{% block roomstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{% trans "Rooms" %}</h1>
|
||||||
|
|
||||||
|
<p><a href="{% url 'room-add' %}" class="btn btn-success">{% trans "Add a room" %}</a><p>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
{% for room in room_list %}
|
||||||
|
<div class="col-xs-6 col-sm-4">
|
||||||
|
<h2>
|
||||||
|
<a href="{{ room.get_absolute_url }}">{{ room }}</a>
|
||||||
|
</h2>
|
||||||
|
{% if room.label %}<p>{{ room.label }}</p>{% endif %}
|
||||||
|
<p>
|
||||||
|
{{ room.capacity }} {% trans "place" %}{{ room.capacity|pluralize }}
|
||||||
|
|
|
||||||
|
<span{% if room.unscheduled_talks %} class="text-danger" data-toggle="tooltip" data-placement="bottom" title="{% trans "Some talks are not scheduled yet." %}"{% endif %}>
|
||||||
|
{{ room.talks.count }} {% trans "talk" %}{{ room.talks.count|pluralize }}
|
||||||
|
</span>
|
||||||
|
|
|
||||||
|
<a href="{% url 'room-edit' room.slug %}">{% bootstrap_icon "pencil" %}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% cycle '' '<div class="clearfix visible-xs"></div>' %}
|
||||||
|
{% cycle '' '' '<div class="clearfix hidden-xs"></div>' %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="col-xs-12"><em>{% trans "No rooms." %}</em></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_end %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function () {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -1,8 +1,7 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block listingtab %} active{% endblock %}
|
{% block talkstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
@ -10,17 +9,17 @@
|
||||||
|
|
||||||
<h3>{% trans "Information about the proposals" %}</h3>
|
<h3>{% trans "Information about the proposals" %}</h3>
|
||||||
<b>{% trans "Title:" %}</b> {{ talk.title }}<br />
|
<b>{% trans "Title:" %}</b> {{ talk.title }}<br />
|
||||||
<b>{% trans "Kind:" %}</b> {{ talk.event }}<br />
|
<b>{% trans "Kind:" %}</b> {{ talk.category }}<br />
|
||||||
|
|
||||||
<h3>{% trans "Information for the proposer" %}</h3>
|
<h3>{% trans "Information for the proposer" %}</h3>
|
||||||
<form action="" method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="message">{% trans "If you want to send a message to the proposer, please enter it below. Remember to indicate which talk your message is reffering." %}</label>
|
<label for="message">{% trans "If you want to send a message to the proposer, please enter it below. Remember to indicate which talk your message is reffering." %}</label>
|
||||||
<textarea name="message" class="form-control" rows="5"></textarea>
|
<textarea name="message" class="form-control" rows="5"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-{% if accept %}success{% else %}danger{% endif %}">{% if accept %}{% trans "Accept the proposal" %}{% else %}{% trans "Decline the proposal" %}{% endif %}</button>
|
<button type="submit" class="btn btn-{% if accept %}success{% else %}danger{% endif %}">{% if accept %}{% trans "Accept the proposal" %}{% else %}{% trans "Decline the proposal" %}{% endif %}</button>
|
||||||
<a class="btn btn-default" href="{% url 'show-talk' talk.slug %}">{% trans "Cancel" %}</a>
|
<a class="btn btn-default" href="{% url 'talk-details' talk.token %}">{% trans "Cancel" %}</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,50 +1,38 @@
|
||||||
{% extends base_template %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load bootstrap3 i18n %}
|
||||||
|
|
||||||
{% if staff %}
|
|
||||||
{% block talkstab %} class="active"{% endblock %}
|
{% block talkstab %} class="active"{% endblock %}
|
||||||
{% else %}
|
|
||||||
{% block exhibitortab %} class="active"{% endblock %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>{{ talk.title }}</h1>
|
<h1>{{ talk.title }}</h1>
|
||||||
|
|
||||||
{% if edit_perm %}
|
<p><a class="btn btn-success" href="{% url 'talk-edit' talk.token %}">{% trans "Edit" %}</a></p>
|
||||||
<a class="btn btn-success" href="{% url 'edit-talk' talk.slug %}">{% trans "Edit" %}</a><br />
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>{% if talk.abstract %}{{ talk.abstract }}{% else %}<i>{% trans "No abstract provided." %}</i>{% endif %}</p>
|
<h3>{% trans "Information" %}</h3>
|
||||||
|
|
||||||
{% if moderate_perm %}
|
|
||||||
|
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>{% trans "Format" %}</dt>
|
<dt>{% trans "Category" %}</dt>
|
||||||
<dd><a href="{{ talk.event.get_absolute_url }}">{{ talk.event }}</a></dd>
|
<dd><a href="{{ talk.category.get_absolute_url }}">{{ talk.category }}</a></dd>
|
||||||
<dt>{% trans "Topics" %}</dt>
|
|
||||||
<dd>{% for topic in talk.topics.all %}
|
<dt>{% trans "Status" %}</dt>
|
||||||
<a href="{{ topic.get_absolute_url }}">{{ topic }}</a>{% if not forloop.last %}, {% endif %}
|
<dd><span class="label label-{{ talk.accepted|yesno:"success,danger,warning" }}">{{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}</span></dd>
|
||||||
{% empty %}
|
|
||||||
<i>{% trans "No topics." %}</i>
|
|
||||||
{% endfor %}</dd>
|
|
||||||
|
|
||||||
<dt>{% trans "Track" %}</dt>
|
<dt>{% trans "Track" %}</dt>
|
||||||
<dd>{% if talk.track %}
|
<dd>{% if talk.track %}
|
||||||
<a href="{{ talk.track.get_absolute_url }}">{{ talk.track }}</a>
|
<a href="{{ talk.track.get_absolute_url }}">{{ talk.track }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<em>{% trans "No assigned yet." %}</em>
|
<em>{% trans "No assigned yet." context "session" %}</em>
|
||||||
{% endif %}</dd>
|
{% endif %}</dd>
|
||||||
|
|
||||||
<dt>Horaire</dt>
|
<dt>{% trans "Timeslot" %}</dt>
|
||||||
<dd>{% if talk.start_date %}
|
<dd>{% if talk.start_date %}
|
||||||
<span class="date">{{ talk.start_date|date:"l d b" }}</span>,
|
<span class="date">{{ talk.start_date|date:"l d b" }}</span>,
|
||||||
<span class="time">{{ talk.start_date|date:"H:i" }} – {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}</span>
|
<span class="time">{{ talk.start_date|date:"H:i" }} – {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}</span>
|
||||||
{% else %}<em>{% trans "not defined" %}</em>
|
{% else %}<em>{% trans "not defined" %}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Salle</dt>
|
<dt>{% trans "Room" %}</dt>
|
||||||
<dd>{% if talk.room %}
|
<dd>{% if talk.room %}
|
||||||
<a href="{{ talk.room.get_absolute_url }}">
|
<a href="{{ talk.room.get_absolute_url }}">
|
||||||
<span class="label label-info">{{ talk.room }}</span>
|
<span class="label label-info">{{ talk.room }}</span>
|
||||||
|
@ -52,6 +40,7 @@
|
||||||
{% else %}<em>{% trans "not defined" %}</em>
|
{% else %}<em>{% trans "not defined" %}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
|
{% comment %}
|
||||||
{% if talk.registration_required %}
|
{% if talk.registration_required %}
|
||||||
<dt>{% trans "Registrations" %}</dt>
|
<dt>{% trans "Registrations" %}</dt>
|
||||||
<dd>{% if talk.attendees_limit %}{{ talk.attendees.count }} / {{ talk.attendees_limit }}{% else %}{% trans "required but unlimited" %}{% endif %}</dd>
|
<dd>{% if talk.attendees_limit %}{{ talk.attendees.count }} / {{ talk.attendees_limit }}{% else %}{% trans "required but unlimited" %}{% endif %}</dd>
|
||||||
|
@ -64,69 +53,49 @@
|
||||||
<dt>{% trans "Video" %}</dt>
|
<dt>{% trans "Video" %}</dt>
|
||||||
<dd><a href="{{ talk.video }}">{% trans "download" %}</a></dd>
|
<dd><a href="{{ talk.video }}">{% trans "download" %}</a></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3>{% trans "Description" %}</h3>
|
<h3>{% trans "Description" %}</h3>
|
||||||
|
|
||||||
<p>{% if talk.description %}{{ talk.description|linebreaksbr }}{% else %}<i>{% trans "No description provided." %}</i>{% endif %}</p>
|
<p>{% if talk.description %}{{ talk.description|linebreaksbr }}{% else %}<i>{% trans "No description provided." %}</i>{% endif %}</p>
|
||||||
|
|
||||||
<h3>{% trans "Speakers" %}</h3>
|
<h3>{% trans "Speakers" %}</h3>
|
||||||
|
|
||||||
{% for speaker in talk.speakers.all %}
|
{% for participant in talk.speakers.all %}
|
||||||
{% if forloop.first %}<ul>{% endif %}
|
{% if forloop.first %}<ul>{% endif %}
|
||||||
<li><a href="{% url 'show-participant' speaker.username %}">{{ speaker.profile }}</a></li>
|
<li><a href="{% url 'participant-details' participant.token %}">{{ participant }}</a></li>
|
||||||
{% if forloop.last %}</ul>{% endif %}
|
{% if forloop.last %}</ul>{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<i>{% trans "No speakers." %}</i>
|
<i>{% trans "No speakers." %}</i>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if moderate_perm %}
|
|
||||||
|
|
||||||
{% if not talk.track %}
|
|
||||||
<h3>{% trans "Track" %}</h3>
|
|
||||||
<p><em>{% trans "No assigned yet." %}</em></p>
|
|
||||||
{% for topic in talk.topics.distinct %}
|
|
||||||
{% if forloop.first %}<p>{% endif %}
|
|
||||||
{% if topic.track %}
|
|
||||||
<a class="btn btn-primary" href="{% url 'assign-talk-to-track' talk.slug topic.track.slug %}">{% trans "Assign to" %} {{ topic.track }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if forloop.last %}</p>{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h3>{% trans "Notes" %}</h3>
|
<h3>{% trans "Notes" %}</h3>
|
||||||
|
|
||||||
<p>{% if talk.notes %}{{ talk.notes|linebreaksbr }}{% else %}<i>{% trans "No notes." %}</i>{% endif %}</p>
|
<p>{% if talk.notes %}{{ talk.notes|linebreaksbr }}{% else %}<i>{% trans "No notes." %}</i>{% endif %}</p>
|
||||||
|
|
||||||
{% if moderate_perm %}
|
|
||||||
|
|
||||||
<h2>{% trans "Moderation" %}</h2>
|
<h2>{% trans "Moderation" %}</h2>
|
||||||
|
|
||||||
<h3>{% trans "Status" %}</h3>
|
<h3>{% trans "Status" %}</h3>
|
||||||
|
|
||||||
<span class="label label-{{ talk.accepted|yesno:"success,danger,warning" }}">{{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}</span><br />
|
|
||||||
|
|
||||||
{% if talk.accepted == None %}
|
{% if talk.accepted == None %}
|
||||||
<h3>{% trans "Vote" %}</h3>
|
<h3>{% trans "Vote" %}</h3>
|
||||||
<div class="btn-group" role="group" aria-label="vote">
|
<div class="btn-group" role="group" aria-label="vote">
|
||||||
<a class="btn {% if vote.vote == -2 %} active {% endif %}btn-danger" href="{% url 'vote' talk=talk.slug score='-2' %}">-2</a>
|
<a class="btn {% if vote.vote == -2 %} active {% endif %}btn-danger" href="{% url 'talk-vote' talk.token '-2' %}">-2</a>
|
||||||
<a class="btn {% if vote.vote == -1 %} active {% endif %}btn-warning" href="{% url 'vote' talk=talk.slug score='-1' %}">-1</a>
|
<a class="btn {% if vote.vote == -1 %} active {% endif %}btn-warning" href="{% url 'talk-vote' talk.token '-1' %}">-1</a>
|
||||||
<a class="btn {% if vote.vote == 0 %} active {% endif %}btn-default" href="{% url 'vote' talk=talk.slug score='0' %}"> 0</a>
|
<a class="btn {% if vote.vote == 0 %} active {% endif %}btn-default" href="{% url 'talk-vote' talk.token '0' %}"> 0</a>
|
||||||
<a class="btn {% if vote.vote == 1 %} active {% endif %}btn-info" href="{% url 'vote' talk=talk.slug score='1' %}">+1</a>
|
<a class="btn {% if vote.vote == 1 %} active {% endif %}btn-info" href="{% url 'talk-vote' talk.token '+1' %}">+1</a>
|
||||||
<a class="btn {% if vote.vote == 2 %} active {% endif %}btn-success" href="{% url 'vote' talk=talk.slug score='2' %}">+2</a>
|
<a class="btn {% if vote.vote == 2 %} active {% endif %}btn-success" href="{% url 'talk-vote' talk.token '+2' %}">+2</a>
|
||||||
</div>
|
</div>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<p>{{ talk.vote_set.all|length }} {% trans "vote" %}{{ talk.vote_set.all|length|pluralize }}, {% trans "average:" %} {{ talk.score|floatformat:1 }}</p>
|
<p>{{ talk.vote_set.count }} {% trans "vote" %}{{ talk.vote_set.count|pluralize }}, {% trans "average:" %} {{ talk.score|floatformat:1 }}</p>
|
||||||
|
|
||||||
<a href="{% url 'accept-talk' talk.slug %}" class="btn btn-success">Accept</a>
|
<a href="{% url 'talk-accept' talk.token %}" class="btn btn-success">Accept</a>
|
||||||
<a href="{% url 'decline-talk' talk.slug %}" class="btn btn-danger">Decline</a>
|
<a href="{% url 'talk-decline' talk.token %}" class="btn btn-danger">Decline</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
{% if talk.registration_required %}
|
{% if talk.registration_required %}
|
||||||
<h3>{% trans "Attendees" %}</h3>
|
<h3>{% trans "Attendees" %}</h3>
|
||||||
|
|
||||||
|
@ -139,15 +108,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
<h3>{% trans "Messages" %}</h3>
|
<h3>{% trans "Messaging" %}</h3>
|
||||||
{% trans "These messages are for organization team only." %}<br /><br />
|
|
||||||
{% for message in talk.conversation.messages.all %}
|
|
||||||
{% include 'conversations/_message_detail.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include 'conversations/_message_form.html' %}
|
{% include 'mailing/_message_list.html' with messages=talk.conversation.message_set.all %}
|
||||||
|
|
||||||
{% endif %}
|
{% trans "Comment this talk – <em>this message will be received by the staff team only</em>" as message_form_title %}
|
||||||
|
{% include 'mailing/_message_form.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
13
cfp/templates/cfp/staff/talk_form.html
Normal file
13
cfp/templates/cfp/staff/talk_form.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load i18n crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block talkstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>{% trans "Edit a talk" %}</h1>
|
||||||
|
|
||||||
|
{% url 'talk-details' talk.token as cancel_url %}
|
||||||
|
{% include '_form.html' %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'staff.html' %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
{% load bootstrap3 i18n %}
|
||||||
{% load bootstrap3 i18n accounts_tags %}
|
|
||||||
|
|
||||||
{% block talkstab %} class="active"{% endblock %}
|
{% block talkstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
@ -16,19 +15,14 @@
|
||||||
<div class="well">
|
<div class="well">
|
||||||
<form class="form-horizontal" method="get">
|
<form class="form-horizontal" method="get">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4 col-xs-6">
|
<div class="col-md-6 col-xs-6">
|
||||||
{% bootstrap_field filter_form.status layout="horizontal" %}
|
{% 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.vote layout="horizontal" %}
|
||||||
{% bootstrap_field filter_form.room layout="horizontal" %}
|
|
||||||
{% bootstrap_field filter_form.scheduled layout="horizontal" %}
|
{% bootstrap_field filter_form.scheduled layout="horizontal" %}
|
||||||
{% bootstrap_field filter_form.materials layout="horizontal" %}
|
{% bootstrap_field filter_form.room layout="horizontal" %}
|
||||||
{% bootstrap_field filter_form.video layout="horizontal" %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 col-xs-6">
|
<div class="col-md-6 col-xs-6">
|
||||||
{% bootstrap_field filter_form.topic layout="horizontal" %}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 col-xs-6">
|
|
||||||
{% bootstrap_field filter_form.track layout="horizontal" %}
|
{% bootstrap_field filter_form.track layout="horizontal" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,17 +31,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="" method="post">
|
|
||||||
|
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover">
|
||||||
<caption>{% trans "Total:" %} {{ talk_list|length }} {% trans "talk" %}{{ talk_list|length|pluralize }}</caption>
|
<caption>{% trans "Total:" %} {{ talk_list|length }} {% trans "talk" %}{{ talk_list|length|pluralize }}</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
{% comment %}<th></th>{% endcomment %}
|
||||||
<th class="text-center">{% trans "Title" %} <a href="?{{ sort_urls.title }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.title }} pull-right"></span></a></th>
|
<th class="text-center">{% trans "Title" %} <a href="?{{ sort_urls.title }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.title }} pull-right"></span></a></th>
|
||||||
<th class="text-center">{% trans "Intervention kind" %} <a href="?{{ sort_urls.kind }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.kind }} pull-right"></span></a></th>
|
<th class="text-center">{% trans "Intervention kind" %} <a href="?{{ sort_urls.category }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.category }} pull-right"></span></a></th>
|
||||||
<th class="text-center">{% trans "Speakers" %}</th>
|
<th class="text-center">{% trans "Speakers" %}</th>
|
||||||
<th class="text-center">{% trans "Topics" %}</th>
|
|
||||||
<th class="text-center">{% trans "Track" %}</th>
|
<th class="text-center">{% trans "Track" %}</th>
|
||||||
<th class="text-center">{% trans "Status" %} <a href="?{{ sort_urls.status }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.status }} pull-right"></span></a></th>
|
<th class="text-center">{% trans "Status" %} <a href="?{{ sort_urls.status }}"><span class="glyphicon glyphicon-{{ sort_glyphicons.status }} pull-right"></span></a></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -57,21 +48,16 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="{{ talk.accepted|yesno:"success,danger,warning" }}">
|
<tr class="{{ talk.accepted|yesno:"success,danger,warning" }}">
|
||||||
<td><input type="checkbox" name="talks" value="{{ talk.slug }}"></td>
|
{% comment %}<td><input type="checkbox" name="talks" value="{{ talk.slug }}"></td>{% endcomment %}
|
||||||
<td><a href="{% url 'show-talk' talk.slug %}">{{ talk.title }}</a></td>
|
<td><a href="{% url 'talk-details' talk.token %}">{{ talk.title }}</a></td>
|
||||||
<td>{{ talk.event }}</td>
|
<td>{{ talk.category }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% for speaker in talk.speakers.all %}
|
{% for participant in talk.speakers.all %}
|
||||||
<a href="{% url 'show-participant' speaker.username %}">{{ speaker.profile }}</a>
|
<a href="{% url 'participant-details' participant.token %}">{{ participant }}</a>
|
||||||
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
|
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
|
||||||
{% empty %}–
|
{% empty %}–
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{% for topic in talk.topics.all %}
|
|
||||||
<span class="badge">{{ topic }}</span>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td>{{ talk.track|default:"–" }}</td>
|
<td>{{ talk.track|default:"–" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if talk.accepted == True %}
|
{% if talk.accepted == True %}
|
||||||
|
@ -89,34 +75,4 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% if action_form %}
|
|
||||||
<div id="filter">
|
|
||||||
<div class="well">
|
|
||||||
<h4>{% trans "For selected talks:" %}</h4>
|
|
||||||
{% csrf_token %}
|
|
||||||
{% bootstrap_field action_form.decision %}
|
|
||||||
{% if request|orga %}
|
|
||||||
{% bootstrap_field action_form.track %}
|
|
||||||
{% bootstrap_field action_form.room %}
|
|
||||||
{% endif %}
|
|
||||||
{% buttons %}
|
|
||||||
<button type="submit" class="btn btn-primary">{% trans "Apply" %}</button>
|
|
||||||
{% endbuttons %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js_end %}
|
|
||||||
<script type="text/javascript">
|
|
||||||
jQuery(document).ready(function($) {
|
|
||||||
var anchor = window.location.hash.replace("#", "");
|
|
||||||
if (anchor == "filter") {
|
|
||||||
$("#filter").collapse('show');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends 'staff.html' %}
|
{% extends 'cfp/staff/base.html' %}
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
{% load bootstrap3 i18n %}
|
||||||
|
|
|
@ -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 %}
|
{% block trackstab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
@ -8,9 +8,7 @@
|
||||||
|
|
||||||
<h1>{% trans "Tracks" %}</h1>
|
<h1>{% trans "Tracks" %}</h1>
|
||||||
|
|
||||||
{% if request|orga %}
|
<p><a href="{% url 'track-add' %}" class="btn btn-success">{% trans "Add a track" %}</a><p>
|
||||||
<p><a href="{% url 'add-track' %}" class="btn btn-success">{% trans "Add a track" %}</a><p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for track in track_list %}
|
{% for track in track_list %}
|
||||||
|
@ -24,7 +22,7 @@
|
||||||
|
|
|
|
||||||
{{ track.estimated_duration|duration_format }}
|
{{ track.estimated_duration|duration_format }}
|
||||||
|
|
|
|
||||||
<a href="{% url 'edit-track' track.slug %}">{% bootstrap_icon "pencil" %}</a>
|
<a href="{% url 'track-edit' track.slug %}">{% bootstrap_icon "pencil" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% cycle '' '<div class="clearfix visible-xs"></div>' %}
|
{% cycle '' '<div class="clearfix visible-xs"></div>' %}
|
|
@ -1,14 +1,14 @@
|
||||||
from django import template
|
from django import template
|
||||||
|
|
||||||
from proposals.utils import markdown_to_html
|
from cfp.utils import is_staff
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.filter
|
||||||
def markdown(value):
|
def staff(request):
|
||||||
return markdown_to_html(value)
|
return is_staff(request, request.user)
|
||||||
|
|
||||||
@register.filter('duration_format')
|
@register.filter('duration_format')
|
||||||
def duration_format(value):
|
def duration_format(value):
|
54
cfp/urls.py
Normal file
54
cfp/urls.py
Normal file
|
@ -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<talk_id>[\w\-]+)/speaker/add/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-add'),
|
||||||
|
url(r'^cfp/(?P<talk_id>[\w\-]+)/speaker/(?P<participant_id>[\w\-]+)/$', views.talk_proposal_speaker_edit, name='talk-proposal-speaker-edit'),
|
||||||
|
url(r'^cfp/(?P<talk_id>[\w\-]+)/(?P<participant_id>[\w\-]+)/$', views.talk_proposal, name='talk-proposal-edit'),
|
||||||
|
url(r'^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<talk_id>[\w\-]+)/$', views.talk_details, name='talk-details'),
|
||||||
|
url(r'^staff/talks/(?P<talk_id>[\w\-]+)/vote/(?P<score>[-+0-2]+)/$', views.talk_vote, name='talk-vote'),
|
||||||
|
url(r'^staff/talks/(?P<talk_id>[\w\-]+)/accept/$', views.talk_decide, {'accept': True}, name='talk-accept'),
|
||||||
|
url(r'^staff/talks/(?P<talk_id>[\w\-]+)/decline/$', views.talk_decide, {'accept': False}, name='talk-decline'),
|
||||||
|
url(r'^staff/talks/(?P<talk_id>[\w\-]+)/edit/$', views.TalkUpdate.as_view(), name='talk-edit'),
|
||||||
|
url(r'^staff/speakers/$', views.participant_list, name='participant-list'),
|
||||||
|
url(r'^staff/speakers/(?P<participant_id>[\w\-]+)/$', views.participant_details, name='participant-details'),
|
||||||
|
url(r'^staff/speakers/(?P<participant_id>[\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<slug>[-\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<slug>[-\w]+)/$', views.RoomDetail.as_view(), name='room-details'),
|
||||||
|
url(r'^staff/rooms/(?P<slug>[-\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<talk>[-\w]+)$', views.talk_edit, name='edit-talk'),
|
||||||
|
#url(r'^talk/vote/(?P<talk>[-\w]+)/(?P<score>[-0-2]+)$', views.vote, name='vote'),
|
||||||
|
#url(r'^talk/details/(?P<slug>[-\w]+)$', views.TalkDetail.as_view(), name='show-talk'),
|
||||||
|
#url(r'^talk/accept/(?P<talk>[-\w]+)/$', views.talk_decide, {'accepted': True}, name='accept-talk'),
|
||||||
|
#url(r'^talk/decline/(?P<talk>[-\w]+)/$', views.talk_decide, {'accepted': False}, name='decline-talk'),
|
||||||
|
#url(r'^talk/assign-to-track/(?P<talk>[-\w]+)/(?P<track>[-\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<slug>[-\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<slug>[-\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<talk>[-\w]+)$', views.talk_register, name='register-for-a-talk'),
|
||||||
|
]
|
|
@ -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 import Q, Sum
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from accounts.models import Participation
|
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
import bleach
|
import bleach
|
||||||
|
|
||||||
|
@ -13,13 +11,22 @@ def query_sum(queryset, field):
|
||||||
return queryset.aggregate(s=Coalesce(Sum(field), 0))['s']
|
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):
|
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))
|
talks = talks.filter(Q(topics__reviewers=request.user) | Q(speakers=request.user) | Q(proposer=request.user))
|
||||||
return talks.distinct()
|
return talks.distinct()
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_html(md):
|
def markdown_to_html(md):
|
||||||
html = markdown(md)
|
html = markdown(md)
|
||||||
allowed_tags = bleach.ALLOWED_TAGS + ['p', 'pre', 'span' ] + ['h%d' % i for i in range(1, 7) ]
|
allowed_tags = bleach.ALLOWED_TAGS + ['p', 'pre', 'span' ] + ['h%d' % i for i in range(1, 7) ]
|
||||||
html = bleach.clean(html, tags=allowed_tags)
|
html = bleach.clean(html, tags=allowed_tags)
|
||||||
return mark_safe(html)
|
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())
|
438
cfp/views.py
Normal file
438
cfp/views.py
Normal file
|
@ -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
|
|
@ -1 +0,0 @@
|
||||||
default_app_config = 'conversations.apps.ConversationsConfig'
|
|
|
@ -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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationsConfig(AppConfig):
|
|
||||||
name = 'conversations'
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import conversations.signals # noqa
|
|
|
@ -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<dest>[a-z0-9]{12})(?P<token>[a-z0-9]{60})(?P<key>[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()
|
|
|
@ -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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -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)
|
|
|
@ -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"
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -1,8 +0,0 @@
|
||||||
<div class="panel panel-{% if message.author == message.conversation.participation.user %}info{% else %}default{% endif %}">
|
|
||||||
<div class="panel-heading">
|
|
||||||
{{ message.created }} | <a href="{% url 'show-participant' message.author.username %}">{{ message.author.profile }}</a>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
{{ message.content|linebreaksbr }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block correspondentstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% blocktrans with correspondent=correspondent.profile %}Conversation with {{ correspondent }}{% endblocktrans %}</h1>
|
|
||||||
|
|
||||||
{% for message in message_list %}
|
|
||||||
{% include 'conversations/_message_detail.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include 'conversations/_message_form.html' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,36 +0,0 @@
|
||||||
{% extends 'staff.html' %}
|
|
||||||
|
|
||||||
{% load bootstrap3 i18n %}
|
|
||||||
|
|
||||||
{% block correspondentstab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% trans "Correspondents" %}</h1>
|
|
||||||
<p>{% trans "This is the list of participants that you follow." %}</p>
|
|
||||||
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Username</th>
|
|
||||||
<th>Full name</th>
|
|
||||||
<th>Administration</th>
|
|
||||||
</tr>
|
|
||||||
{% for correspondent in correspondent_list %}
|
|
||||||
<tr>
|
|
||||||
<th>{{ forloop.counter }}</th>
|
|
||||||
<td>{{ correspondent.user.username }}</td>
|
|
||||||
<td>{{ correspondent.user.get_full_name }}</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'user-conversation' correspondent.user.username %}"><span class="glyphicon glyphicon-envelope"></span></a>
|
|
||||||
{% if request.user in correspondent.conversation.subscribers.all %}
|
|
||||||
<a href="{% url 'unsubscribe-conversation' correspondent.user.username %}?next={% url 'list-correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="{% trans "Unsubscribe from the conversation" %}"><span class="glyphicon glyphicon-star"></span></a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'subscribe-conversation' correspondent.user.username %}?next={% url 'list-correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="{% trans "Subscribe to the conversation" %}"><span class="glyphicon glyphicon-star-empty"></span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{{ content|linebreaksbr }}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
{% if answering %}
|
|
||||||
Reply to this email directly or <a href="https://{{ uri }}">view it online</a>.
|
|
||||||
{% else %}
|
|
||||||
<a href="https://{{ uri }}">Reply online</a>.
|
|
||||||
{% endif %}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{{ content|safe }}
|
|
||||||
|
|
||||||
--
|
|
||||||
Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }}
|
|
|
@ -1,11 +0,0 @@
|
||||||
Hi!<br />
|
|
||||||
<br />
|
|
||||||
A <a href="https://{{ uri }}">new {{ talk.event }}</a> has been proposed by <a href="https://{{ proposer_uri }}">{{ proposer.profile }}</a>!<br />
|
|
||||||
<br />
|
|
||||||
Title: {{ talk.title }}<br />
|
|
||||||
<br />
|
|
||||||
Description:<br />
|
|
||||||
<p>{{ talk.description|linebreaksbr }}</p>
|
|
||||||
{% if answering %}
|
|
||||||
<hr>
|
|
||||||
Reply to this email directly to comment this talk.{% endif %}
|
|
|
@ -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 %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block inboxtab %} class="active"{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h1>{% trans "Messaging" %}</h1>
|
|
||||||
<p>{% trans "You can use this page to communicate with the staff." %}</p>
|
|
||||||
|
|
||||||
{% for message in message_list %}
|
|
||||||
{% include 'conversations/_message_detail.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% include 'conversations/_message_form.html' %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -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()))
|
|
|
@ -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<username>[\w.@+-]+)/$', views.user_conversation, name='user-conversation'),
|
|
||||||
url(r'^about/(?P<talk>[\w.@+-]+)/$', views.talk_conversation, name='talk-conversation'),
|
|
||||||
url(r'^subscribe/(?P<username>[\w.@+-]+)/$', views.subscribe, name='subscribe-conversation'),
|
|
||||||
url(r'^unsubscribe/(?P<username>[\w.@+-]+)/$', views.unsubscribe, name='unsubscribe-conversation'),
|
|
||||||
]
|
|
|
@ -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)
|
|
|
@ -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)
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
6
mailing/admin.py
Normal file
6
mailing/admin.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Message
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(Message)
|
|
@ -2,4 +2,5 @@ from django.forms.models import modelform_factory
|
||||||
|
|
||||||
from .models import Message
|
from .models import Message
|
||||||
|
|
||||||
|
|
||||||
MessageForm = modelform_factory(Message, fields=['content'])
|
MessageForm = modelform_factory(Message, fields=['content'])
|
0
mailing/management/commands/__init__.py
Normal file
0
mailing/management/commands/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user