Merge branch 'v2'

This commit is contained in:
Élie Bouttier 2017-08-12 14:47:18 +02:00
commit 8a8474cd8c
148 changed files with 3519 additions and 4161 deletions

View File

@ -1 +0,0 @@
default_app_config = 'accounts.apps.AccountsConfig'

View File

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

View File

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

View File

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

View File

@ -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')]),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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

View File

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

View File

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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -1 +0,0 @@
<img src="{{ url }}" alt="{{ alt }}" width="{{ size }}" height="{{ size }}" />

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

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

View File

@ -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')),
]

View File

@ -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()

View File

@ -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
View File

@ -0,0 +1 @@
default_app_config = 'cfp.apps.CFPConfig'

43
cfp/admin.py Normal file
View 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
View 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)

View File

@ -0,0 +1,4 @@
def conference(request):
return {'conference': request.conference}

View File

@ -3,18 +3,7 @@ from functools import wraps
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from accounts.utils import is_orga, is_staff
def orga_required(view_func):
def _is_orga(request, *args, **kwargs):
if not request.user.is_authenticated():
return login_required(view_func)(request, *args, **kwargs)
elif is_orga(request, request.user):
return view_func(request, *args, **kwargs)
else:
raise PermissionDenied
return wraps(view_func)(_is_orga)
from cfp.utils import is_staff
def staff_required(view_func):

191
cfp/forms.py Normal file
View 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
View 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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2017-01-13 10:49
# Generated by Django 1.11 on 2017-06-06 22:14
from __future__ import unicode_literals
import autoslug.fields
@ -8,7 +8,7 @@ from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import proposals.models
import uuid
class Migration(migrations.Migration):
@ -16,39 +16,72 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('planning', '0001_initial'),
('sites', '0002_alter_domain_unique'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Attendee',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(blank=True, default='', max_length=64)),
('email', models.EmailField(blank=True, default='', max_length=254)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Conference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=100)),
('home', models.TextField(blank=True, default='')),
('venue', models.TextField(blank=True, default='')),
('city', models.CharField(blank=True, default='', max_length=64)),
('subscriptions_open', models.BooleanField(default=False)),
('contact_email', models.CharField(blank=True, max_length=100)),
('custom_css', models.TextField(blank=True)),
('external_css_link', models.URLField(blank=True)),
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
migrations.CreateModel(
name='Event',
name='Participant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=128, verbose_name='Your Name')),
('email', models.EmailField(max_length=254)),
('biography', models.TextField(verbose_name='Biography')),
('token', models.UUIDField(default=uuid.uuid4, editable=False)),
('twitter', models.CharField(blank=True, default='', max_length=100, verbose_name='Twitter')),
('linkedin', models.CharField(blank=True, default='', max_length=100, verbose_name='LinkedIn')),
('github', models.CharField(blank=True, default='', max_length=100, verbose_name='Github')),
('website', models.CharField(blank=True, default='', max_length=100, verbose_name='Website')),
('facebook', models.CharField(blank=True, default='', max_length=100, verbose_name='Facebook')),
('mastodon', models.CharField(blank=True, default='', max_length=100, verbose_name='Mastodon')),
('phone_number', models.CharField(blank=True, default='', max_length=64, verbose_name='Phone number')),
('language', models.CharField(blank=True, max_length=10)),
('notes', models.TextField(blank=True, default='', help_text='This field is only visible by organizers.', verbose_name='Notes')),
('vip', models.BooleanField(default=False)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
migrations.CreateModel(
name='Talk',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=128, verbose_name='Talk Title')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
('description', models.TextField(verbose_name='Description of your talk')),
('notes', models.TextField(blank=True, help_text='If you have any constraint or if you have anything that may help you to select your talk, like a video or slides of your talk, please write it down here', verbose_name='Message to organizers')),
('videotaped', models.BooleanField(default=True, verbose_name="I'm ok to be recorded on video")),
('video_licence', models.CharField(choices=[('CC-Zero CC-BY', 'CC-Zero CC-BY'), ('CC-BY-SA', 'CC-BY-SA'), ('CC-BY-ND', 'CC-BY-ND'), ('CC-BY-NC', 'CC-BY-NC'), ('CC-BY-NC-SA', 'CC-BY-NC-SA'), ('CC-BY-NC-ND', 'CC-BY-NC-ND')], default='CC-BY-SA', max_length=10, verbose_name='Video licence')),
('sound', models.BooleanField(default=False, verbose_name='I need sound')),
('accepted', models.NullBooleanField(default=None)),
('duration', models.PositiveIntegerField(default=0, verbose_name='Duration (min)')),
('plenary', models.BooleanField(default=False)),
('token', models.UUIDField(default=uuid.uuid4, editable=False)),
],
options={
'ordering': ('category__id', 'title'),
},
),
migrations.CreateModel(
name='TalkCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=64)),
@ -60,51 +93,11 @@ class Migration(migrations.Migration):
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
options={
'verbose_name': 'category',
'verbose_name_plural': 'categories',
'ordering': ('pk',),
},
),
migrations.CreateModel(
name='Talk',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('title', models.CharField(help_text='After submission, title can only be changed by the staff.', max_length=128, verbose_name='Title')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
('abstract', models.CharField(blank=True, max_length=255, verbose_name='Abstract')),
('description', models.TextField(blank=True, verbose_name='Description')),
('notes', models.TextField(blank=True, verbose_name='Notes')),
('accepted', models.NullBooleanField(default=None)),
('start_date', models.DateTimeField(blank=True, default=None, null=True)),
('duration', models.PositiveIntegerField(default=0, verbose_name='Duration (min)')),
('plenary', models.BooleanField(default=False)),
('registration_required', models.BooleanField(default=False)),
('attendees_limit', models.PositiveIntegerField(default=0, verbose_name='Max. number of attendees')),
('materials', models.FileField(help_text='You can use this field to share some materials related to your intervention.', null=True, upload_to=proposals.models.talk_materials_destination, verbose_name='Materials')),
('attendees', models.ManyToManyField(to='proposals.Attendee', verbose_name='Attendees')),
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Event', verbose_name='Intervention kind')),
('proposer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
('room', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='planning.Room')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
('speakers', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Speakers')),
],
options={
'ordering': ('event__id',),
},
),
migrations.CreateModel(
name='Topic',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name', unique=True)),
('description', models.TextField(blank=True, verbose_name='Description')),
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
migrations.CreateModel(
name='Track',
fields=[
@ -114,7 +107,6 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('managers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Managers')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
],
),
@ -125,24 +117,29 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('vote', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(-2), django.core.validators.MaxValueValidator(2)])),
('talk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Talk')),
('talk', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cfp.Talk')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='topic',
name='track',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='proposals.Track', verbose_name='Destination track'),
model_name='talk',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cfp.TalkCategory', verbose_name='Talk Category'),
),
migrations.AddField(
model_name='talk',
name='topics',
field=models.ManyToManyField(blank=True, help_text='The topics can not be changed after submission.', to='proposals.Topic', verbose_name='Topics'),
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
),
migrations.AddField(
model_name='talk',
name='speakers',
field=models.ManyToManyField(to='cfp.Participant', verbose_name='Speakers'),
),
migrations.AddField(
model_name='talk',
name='track',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='proposals.Track', verbose_name='Track'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cfp.Track', verbose_name='Track'),
),
migrations.AlterUniqueTogether(
name='vote',
@ -153,11 +150,11 @@ class Migration(migrations.Migration):
unique_together=set([('site', 'name')]),
),
migrations.AlterUniqueTogether(
name='topic',
name='talkcategory',
unique_together=set([('site', 'name')]),
),
migrations.AlterUniqueTogether(
name='event',
unique_together=set([('site', 'name')]),
name='participant',
unique_together=set([('site', 'email')]),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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'),
),
]

View 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),
),
]

View 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'),
),
]

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2017-01-13 10:49
# Generated by Django 1.11.3 on 2017-08-11 23:42
from __future__ import unicode_literals
import autoslug.fields
@ -9,10 +9,9 @@ import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('sites', '0002_alter_domain_unique'),
('cfp', '0007_conference_secure_domain'),
]
operations = [
@ -30,6 +29,21 @@ class Migration(migrations.Migration):
'ordering': ['name'],
},
),
migrations.AddField(
model_name='talk',
name='start_date',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Beginning date and time'),
),
migrations.AlterField(
model_name='conference',
name='secure_domain',
field=models.BooleanField(default=True, verbose_name='Secure domain (HTTPS)'),
),
migrations.AddField(
model_name='talk',
name='room',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='cfp.Room'),
),
migrations.AlterUniqueTogether(
name='room',
unique_together=set([('site', 'name')]),

13
cfp/mixins.py Normal file
View 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
View 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
View 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()

View 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 %}

View 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 %}

View File

@ -1,11 +1,11 @@
{% extends 'base.html' %}
{% load proposals_tags i18n %}
{% load ponyconf_tags i18n %}
{% block hometab %} class="active"{% endblock %}
{% block content %}
{% markdown site.conference.home %}
{% markdown conference.home %}
{% endblock %}

View 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 %}

View 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 %}

View File

@ -6,16 +6,24 @@
{% block navbar %}
{{ block.super }}
<ul class="nav nav-tabs nav-justified subnav">
{% comment %}
<li{% block topicstab %}{% endblock %}><a href="{% url 'list-topics' %}"><span class="glyphicon glyphicon-tag"></span>&nbsp;{% trans "Topics" %}</a></li>
<li{% block trackstab %}{% endblock %}><a href="{% url 'list-tracks' %}"><span class="glyphicon glyphicon-screenshot"></span>&nbsp;{% trans "Tracks" %}</a></li>
<li{% block talkstab %}{% endblock %}><a href="{% url 'list-talks' %}"><span class="glyphicon glyphicon-blackboard"></span>&nbsp;{% trans "Talks" %}</a></li>
<li{% block speakerstab %}{% endblock %}><a href="{% url 'list-speakers' %}"><span class="glyphicon glyphicon-bullhorn"></span>&nbsp;{% trans "Speakers" %}</a></li>
<li{% block volunteerstab %}{% endblock %}><a href="{% url 'list-volunteers' %}"><span class="glyphicon glyphicon-thumbs-up"></span>&nbsp;{% trans "Volunteers" %}</a></li>
<li{% block roomstab %}{% endblock %}><a href="{% url 'list-rooms' %}"><span class="glyphicon glyphicon-tent"></span>&nbsp;{% trans "Rooms" %}</a></li>
<li{% block scheduletab %}{% endblock %}><a href="{% url 'show-schedule' %}"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{% trans "Schedule" %}</a></li>
<li{% block participantstab %}{% endblock %}><a href="{% url 'list-participants' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;{% trans "Participants" %}</a></li>
<li{% block correspondentstab %}{% endblock %}><a href="{% url 'list-correspondents' %}"><span class="glyphicon glyphicon-envelope"></span>&nbsp;{% trans "Correspondents" %}</a></li>
<li{% block conferencetab %}{% endblock %}><a href="{% url 'edit-conference' %}"><span class="glyphicon glyphicon-cog"></span>&nbsp;{% trans "Conference" %}</a></li>
{% endcomment %}
<li{% block talkstab %}{% endblock %}><a href="{% url 'talk-list' %}"><span class="glyphicon glyphicon-blackboard"></span>&nbsp;{% trans "Talks" %}</a></li>
<li{% block speakerstab %}{% endblock %}><a href="{% url 'participant-list' %}"><span class="glyphicon glyphicon-bullhorn"></span>&nbsp;{% trans "Speakers" %}</a></li>
<li{% block trackstab %}{% endblock %}><a href="{% url 'track-list' %}"><span class="glyphicon glyphicon-screenshot"></span>&nbsp;{% trans "Tracks" %}</a></li>
<li{% block roomstab %}{% endblock %}><a href="{% url 'room-list' %}"><span class="glyphicon glyphicon-tent"></span>&nbsp;{% trans "Rooms" %}</a></li>
<li{% block conferencetab %}{% endblock %}><a href="{% url 'conference' %}"><span class="glyphicon glyphicon-asterisk"></span>&nbsp;{% trans "Conference" %}</a></li>
{% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-dashboard"></span>&nbsp;Django-Admin</a></li>
{% endif %}
</ul>
{% endblock %}
{% block content %}
{% trans "Please select a category." %}
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

@ -1,5 +1,4 @@
{% extends 'staff.html' %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
{% block speakerstab %} class="active"{% endblock %}
@ -8,6 +7,7 @@
<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>
<br /><br />
@ -39,63 +39,43 @@
</form>
</div>
</div>
{% endcomment %}
<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>
<thead>
<tr>
<th class="text-center">{% trans "Username" %}</th>
<th class="text-center">{% trans "Fullname" %}</th>
<th class="text-center">{% trans "Name" %}</th>
<th class="text-center">{% trans "Talk count" %}</th>
<th class="text-center">{% blocktrans context "table column title" %}Need transport?{% endblocktrans %}</th>
<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>
{% comment %}<th class="text-center"></th>{% endcomment %}
</tr>
</thead>
{% comment %}
<tfoot>
<tr>
<td colspan="7">{% trans "Contact:" %} <a href="{{ contact_link }}">{% trans "link" %}</a></td>
</tr>
</tfoot>
{% for speaker in speaker_list %}
{% endcomment %}
{% for participant in participant_list %}
{% if forloop.first %}
<tbody>
{% endif %}
<tr>
<td><a href="{% url 'show-participant' username=speaker.user.username %}">{{ speaker.user.username }}</a></td>
<td>{{ speaker.user.get_full_name }}</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>
{% if speaker.need_transport %}
<td class="{% if speaker.transport_booked %}success{% else %}warning{% endif %}">
{% for transport in speaker.transport.all %}
{% if not forloop.first %}, {% endif %}
{{ transport }}
{% empty %}
Yes
{% endfor %}
<td><a href="{% url 'participant-details' participant.token %}">{{ participant }}</a></td>
<td>
<span class="text-success">{% blocktrans count accepted=participant.accepted_talk_count %}accepted: {{ accepted }}{% plural %}accepted: {{ accepted }}{% endblocktrans %}</span>
<span class="text-warning">{% blocktrans count pending=participant.pending_talk_count %}pending: {{ pending }}{% plural %}pending: {{ pending }}{% endblocktrans %}</span>
<span class="text-danger">{% blocktrans count refused=participant.refused_talk_count %}refused: {{ refused }}{% plural %}refused: {{ refused }}{% endblocktrans %}</span>
</td>
{% elif speaker.need_transport is None %}
<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 %}
{% comment %}
<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>
</td>
{% endcomment %}
</tr>
{% if forloop.last %}
</tbody>
@ -105,6 +85,7 @@
{% endblock %}
{% comment %}
{% block js_end %}
<script type="text/javascript">
jQuery(document).ready(function($) {
@ -115,3 +96,4 @@ jQuery(document).ready(function($) {
});
</script>
{% endblock %}
{% endcomment %}

View 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 %} &ndash; <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 %}
&ndash; <span>{{ talk.start_date }} &ndash; {% 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 %} &ndash; <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 %}

View File

@ -1,8 +1,8 @@
{% extends 'staff.html' %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
{% block topicstab %} class="active"{% endblock %}
{% block roomstab %} class="active"{% endblock %}
{% block css %}
{{ block.super }}
@ -11,7 +11,7 @@
{% block content %}
<h1>{% trans "Topic" %}</h1>
<h1>{% trans "Room" %}</h1>
{% include "_form.html" %}

View 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 %}

View File

@ -1,8 +1,7 @@
{% extends 'base.html' %}
{% extends 'cfp/staff/base.html' %}
{% load i18n %}
{% block listingtab %} active{% endblock %}
{% block talkstab %} class="active"{% endblock %}
{% block content %}
@ -10,17 +9,17 @@
<h3>{% trans "Information about the proposals" %}</h3>
<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>
<form action="" method="post">
<form method="post">
{% csrf_token %}
<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>
<textarea name="message" class="form-control" rows="5"></textarea>
</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>
<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>
{% endblock %}

View File

@ -1,50 +1,38 @@
{% extends base_template %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
{% if staff %}
{% block talkstab %} class="active"{% endblock %}
{% else %}
{% block exhibitortab %} class="active"{% endblock %}
{% endif %}
{% load i18n %}
{% block content %}
<h1>{{ talk.title }}</h1>
{% if edit_perm %}
<a class="btn btn-success" href="{% url 'edit-talk' talk.slug %}">{% trans "Edit" %}</a><br />
{% endif %}
<p><a class="btn btn-success" href="{% url 'talk-edit' talk.token %}">{% trans "Edit" %}</a></p>
<p>{% if talk.abstract %}{{ talk.abstract }}{% else %}<i>{% trans "No abstract provided." %}</i>{% endif %}</p>
{% if moderate_perm %}
<h3>{% trans "Information" %}</h3>
<dl class="dl-horizontal">
<dt>{% trans "Format" %}</dt>
<dd><a href="{{ talk.event.get_absolute_url }}">{{ talk.event }}</a></dd>
<dt>{% trans "Topics" %}</dt>
<dd>{% for topic in talk.topics.all %}
<a href="{{ topic.get_absolute_url }}">{{ topic }}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
<i>{% trans "No topics." %}</i>
{% endfor %}</dd>
<dt>{% trans "Category" %}</dt>
<dd><a href="{{ talk.category.get_absolute_url }}">{{ talk.category }}</a></dd>
<dt>{% trans "Status" %}</dt>
<dd><span class="label label-{{ talk.accepted|yesno:"success,danger,warning" }}">{{ talk.accepted|yesno:"Accepted,Declined,Pending decision" }}</span></dd>
<dt>{% trans "Track" %}</dt>
<dd>{% if talk.track %}
<a href="{{ talk.track.get_absolute_url }}">{{ talk.track }}</a>
{% else %}
<em>{% trans "No assigned yet." %}</em>
<em>{% trans "No assigned yet." context "session" %}</em>
{% endif %}</dd>
<dt>Horaire</dt>
<dt>{% trans "Timeslot" %}</dt>
<dd>{% if talk.start_date %}
<span class="date">{{ talk.start_date|date:"l d b" }}</span>,
<span class="time">{{ talk.start_date|date:"H:i" }} &ndash; {% if talk.end_date %}{{ talk.end_date|date:"H:i" }}{% else %}?{% endif %}</span>
{% else %}<em>{% trans "not defined" %}</em>
{% endif %}
</dd>
<dt>Salle</dt>
<dt>{% trans "Room" %}</dt>
<dd>{% if talk.room %}
<a href="{{ talk.room.get_absolute_url }}">
<span class="label label-info">{{ talk.room }}</span>
@ -52,6 +40,7 @@
{% else %}<em>{% trans "not defined" %}</em>
{% endif %}
</dd>
{% comment %}
{% if talk.registration_required %}
<dt>{% trans "Registrations" %}</dt>
<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>
<dd><a href="{{ talk.video }}">{% trans "download" %}</a></dd>
{% endif %}
{% endcomment %}
</dl>
{% endif %}
<h3>{% trans "Description" %}</h3>
<p>{% if talk.description %}{{ talk.description|linebreaksbr }}{% else %}<i>{% trans "No description provided." %}</i>{% endif %}</p>
<h3>{% trans "Speakers" %}</h3>
{% for speaker in talk.speakers.all %}
{% for participant in talk.speakers.all %}
{% 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 %}
{% empty %}
<i>{% trans "No speakers." %}</i>
{% 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>
<p>{% if talk.notes %}{{ talk.notes|linebreaksbr }}{% else %}<i>{% trans "No notes." %}</i>{% endif %}</p>
{% if moderate_perm %}
<h2>{% trans "Moderation" %}</h2>
<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 %}
<h3>{% trans "Vote" %}</h3>
<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 == -1 %} active {% endif %}btn-warning" href="{% url 'vote' talk=talk.slug score='-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 == 1 %} active {% endif %}btn-info" href="{% url 'vote' talk=talk.slug score='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-danger" href="{% url 'talk-vote' talk.token '-2' %}">-2</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 'talk-vote' talk.token '0' %}"> 0</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 'talk-vote' talk.token '+2' %}">+2</a>
</div>
<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 'decline-talk' talk.slug %}" class="btn btn-danger">Decline</a>
<a href="{% url 'talk-accept' talk.token %}" class="btn btn-success">Accept</a>
<a href="{% url 'talk-decline' talk.token %}" class="btn btn-danger">Decline</a>
{% endif %}
{% comment %}
{% if talk.registration_required %}
<h3>{% trans "Attendees" %}</h3>
@ -139,15 +108,13 @@
{% endfor %}
{% endif %}
{% endcomment %}
<h3>{% trans "Messages" %}</h3>
{% trans "These messages are for organization team only." %}<br /><br />
{% for message in talk.conversation.messages.all %}
{% include 'conversations/_message_detail.html' %}
{% endfor %}
<h3>{% trans "Messaging" %}</h3>
{% 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 %}

View 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 %}

View File

@ -1,6 +1,5 @@
{% extends 'staff.html' %}
{% load bootstrap3 i18n accounts_tags %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}
{% block talkstab %} class="active"{% endblock %}
@ -16,19 +15,14 @@
<div class="well">
<form class="form-horizontal" method="get">
<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.kind layout="horizontal" %}
{% bootstrap_field filter_form.category layout="horizontal" %}
{% bootstrap_field filter_form.vote layout="horizontal" %}
{% bootstrap_field filter_form.room layout="horizontal" %}
{% bootstrap_field filter_form.scheduled layout="horizontal" %}
{% bootstrap_field filter_form.materials layout="horizontal" %}
{% bootstrap_field filter_form.video layout="horizontal" %}
{% bootstrap_field filter_form.room layout="horizontal" %}
</div>
<div class="col-md-4 col-xs-6">
{% bootstrap_field filter_form.topic layout="horizontal" %}
</div>
<div class="col-md-4 col-xs-6">
<div class="col-md-6 col-xs-6">
{% bootstrap_field filter_form.track layout="horizontal" %}
</div>
</div>
@ -37,17 +31,14 @@
</div>
</div>
<form action="" method="post">
<table class="table table-bordered table-hover">
<caption>{% trans "Total:" %} {{ talk_list|length }} {% trans "talk" %}{{ talk_list|length|pluralize }}</caption>
<thead>
<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 "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 "Topics" %}</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>
</tr>
@ -57,21 +48,16 @@
<tbody>
{% endif %}
<tr class="{{ talk.accepted|yesno:"success,danger,warning" }}">
<td><input type="checkbox" name="talks" value="{{ talk.slug }}"></td>
<td><a href="{% url 'show-talk' talk.slug %}">{{ talk.title }}</a></td>
<td>{{ talk.event }}</td>
{% comment %}<td><input type="checkbox" name="talks" value="{{ talk.slug }}"></td>{% endcomment %}
<td><a href="{% url 'talk-details' talk.token %}">{{ talk.title }}</a></td>
<td>{{ talk.category }}</td>
<td>
{% for speaker in talk.speakers.all %}
<a href="{% url 'show-participant' speaker.username %}">{{ speaker.profile }}</a>
{% for participant in talk.speakers.all %}
<a href="{% url 'participant-details' participant.token %}">{{ participant }}</a>
{% if forloop.revcounter == 2 %} {% trans "and" %} {% elif not forloop.last %}, {% endif %}
{% empty %}
{% endfor %}
</td>
<td>
{% for topic in talk.topics.all %}
<span class="badge">{{ topic }}</span>
{% endfor %}
</td>
<td>{{ talk.track|default:"" }}</td>
<td>
{% if talk.accepted == True %}
@ -89,34 +75,4 @@
{% endfor %}
</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 %}

View File

@ -1,4 +1,4 @@
{% extends 'staff.html' %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 i18n %}

View File

@ -1,6 +1,6 @@
{% extends 'staff.html' %}
{% extends 'cfp/staff/base.html' %}
{% load bootstrap3 accounts_tags proposals_tags i18n %}
{% load bootstrap3 cfp_tags i18n %}
{% block trackstab %} class="active"{% endblock %}
@ -8,9 +8,7 @@
<h1>{% trans "Tracks" %}</h1>
{% if request|orga %}
<p><a href="{% url 'add-track' %}" class="btn btn-success">{% trans "Add a track" %}</a><p>
{% endif %}
<p><a href="{% url 'track-add' %}" class="btn btn-success">{% trans "Add a track" %}</a><p>
<div class="row">
{% for track in track_list %}
@ -24,7 +22,7 @@
|
{{ 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 %}
</div>
{% cycle '' '<div class="clearfix visible-xs"></div>' %}

View File

@ -1,14 +1,14 @@
from django import template
from proposals.utils import markdown_to_html
from cfp.utils import is_staff
register = template.Library()
@register.simple_tag
def markdown(value):
return markdown_to_html(value)
@register.filter
def staff(request):
return is_staff(request, request.user)
@register.filter('duration_format')
def duration_format(value):

54
cfp/urls.py Normal file
View 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'),
]

View File

@ -1,10 +1,8 @@
from django.contrib.sites.shortcuts import get_current_site
from django.utils.crypto import get_random_string
from django.db.models import Q, Sum
from django.db.models.functions import Coalesce
from django.utils.safestring import mark_safe
from accounts.models import Participation
from markdown import markdown
import bleach
@ -13,13 +11,22 @@ def query_sum(queryset, field):
return queryset.aggregate(s=Coalesce(Sum(field), 0))['s']
def generate_user_uid():
return get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')
def allowed_talks(talks, request):
if not Participation.objects.get(site=get_current_site(request), user=request.user).is_orga():
if not Participation.objects.get(site=request.conference.site, user=request.user).is_orga():
talks = talks.filter(Q(topics__reviewers=request.user) | Q(speakers=request.user) | Q(proposer=request.user))
return talks.distinct()
def markdown_to_html(md):
html = markdown(md)
allowed_tags = bleach.ALLOWED_TAGS + ['p', 'pre', 'span' ] + ['h%d' % i for i in range(1, 7) ]
html = bleach.clean(html, tags=allowed_tags)
return mark_safe(html)
def is_staff(request, user):
return user.is_authenticated and (user.is_superuser or request.conference.staff.filter(pk=user.pk).exists())

438
cfp/views.py Normal file
View 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

View File

@ -1 +0,0 @@
default_app_config = 'conversations.apps.ConversationsConfig'

View File

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

View File

@ -1,8 +0,0 @@
from django.apps import AppConfig
class ConversationsConfig(AppConfig):
name = 'conversations'
def ready(self):
import conversations.signals # noqa

View File

@ -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()

View File

@ -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'],
},
),
]

View File

@ -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'),
),
]

View File

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

View File

@ -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"

View File

@ -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),
}
)

View File

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

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -1,4 +0,0 @@
{{ content|safe }}
--
Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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()))

View File

@ -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'),
]

View File

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

View File

@ -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
View File

@ -0,0 +1,6 @@
from django.contrib import admin
from .models import Message
admin.site.register(Message)

View File

@ -1,5 +1,6 @@
from django.forms.models import modelform_factory
from .models import Message
MessageForm = modelform_factory(Message, fields=['content'])

View File

Some files were not shown because too many files have changed in this diff Show More