conversation app (continuation)

This commit is contained in:
Élie Bouttier 2016-06-14 23:58:04 +02:00
parent 2ceadcd96b
commit a7d1ccd863
35 changed files with 581 additions and 144 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
*.pyc *.pyc
*.swp *.swp
*.sqlite3 *.sqlite3
ponyconf/*_settings.py

View File

@ -5,4 +5,4 @@ class AccountsConfig(AppConfig):
name = 'accounts' name = 'accounts'
def ready(self): def ready(self):
import accounts.signals # noqa import accounts.signals

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-13 18:50 # Generated by Django 1.9.7 on 2016-06-14 19:13
from __future__ import unicode_literals from __future__ import unicode_literals
import accounts.models import accounts.utils
from django.conf import settings from django.conf import settings
import django.contrib.sites.managers import django.contrib.sites.managers
from django.db import migrations, models from django.db import migrations, models
@ -16,7 +16,6 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sites', '0002_alter_domain_unique'),
] ]
operations = [ operations = [
@ -29,8 +28,6 @@ class Migration(migrations.Migration):
('transport', models.IntegerField(blank=True, choices=[(1, 'train'), (2, 'plane')], null=True)), ('transport', models.IntegerField(blank=True, choices=[(1, 'train'), (2, 'plane')], null=True)),
('connector', models.IntegerField(blank=True, choices=[(1, 'VGA'), (2, 'HDMI'), (3, 'miniDP')], null=True)), ('connector', models.IntegerField(blank=True, choices=[(1, 'VGA'), (2, 'HDMI'), (3, 'miniDP')], null=True)),
('constraints', models.TextField(blank=True)), ('constraints', models.TextField(blank=True)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
managers=[ managers=[
('objects', django.db.models.manager.Manager()), ('objects', django.db.models.manager.Manager()),
@ -42,12 +39,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('biography', models.TextField(blank=True, verbose_name='Biography')), ('biography', models.TextField(blank=True, verbose_name='Biography')),
('email_token', models.CharField(default=accounts.models.generate_user_uid, max_length=12)), ('email_token', models.CharField(default=accounts.utils.generate_user_uid, max_length=12)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.AlterUniqueTogether(
name='participation',
unique_together=set([('site', 'user')]),
),
] ]

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-14 19:13
from __future__ import unicode_literals
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),
('proposals', '0001_initial'),
('accounts', '0001_initial'),
('sites', '0002_alter_domain_unique'),
]
operations = [
migrations.AddField(
model_name='participation',
name='review_topics',
field=models.ManyToManyField(blank=True, to='proposals.Topic'),
),
migrations.AddField(
model_name='participation',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site'),
),
migrations.AddField(
model_name='participation',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='participation',
unique_together=set([('site', 'user')]),
),
]

View File

@ -5,16 +5,12 @@ from django.contrib.sites.managers import CurrentSiteManager
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.utils.crypto import get_random_string
__all__ = ['Profile', 'Participation'] from .utils import enum_to_choices, generate_user_uid
from proposals.models import Topic
def enum_to_choices(enum): __all__ = ['Profile']
return ((item.value, item.name) for item in list(enum))
def generate_user_uid():
return get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')
class Profile(models.Model): class Profile(models.Model):
@ -45,6 +41,9 @@ class Participation(models.Model):
connector = models.IntegerField(choices=enum_to_choices(CONNECTORS), blank=True, null=True) connector = models.IntegerField(choices=enum_to_choices(CONNECTORS), blank=True, null=True)
constraints = models.TextField(blank=True) constraints = models.TextField(blank=True)
# Participe as reviewer for theses topics
review_topics = models.ManyToManyField(Topic, blank=True)
objects = models.Manager() objects = models.Manager()
on_site = CurrentSiteManager() on_site = CurrentSiteManager()

View File

@ -0,0 +1,45 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block admintab %}active{% endblock %}
{% block content %}
<div class="page-header">
<h1>Participants</h1>
</div>
<table class="table table-striped">
<tr>
<th>#</th>
<th>Username</th>
<th>Fullname</th>
<th>Administration</th>
</tr>
{% for participation in participation_list %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ participation.user.username }}</td>
<td>{{ participation.user.get_full_name }}</td>
<td>
<a href="{% url 'conversation' participation.user.username %}" data-toggle="tooltip" data-placement="bottom" title="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 'participants' %}" data-toggle="tooltip" data-placement="bottom" title="Unsubscribe to conversation"><span class="glyphicon glyphicon-star"></span></a>
{% else %}
<a href="{% url 'subscribe-conversation' participation.user.username %}?next={% url 'participants' %}" data-toggle="tooltip" data-placement="bottom" title="Subscribe to conversation"><span class="glyphicon glyphicon-star-empty"></span></a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block js_end %}
<script type="text/javascript">
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}

View File

@ -2,6 +2,8 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% block logintab %} class="active"{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">

View File

@ -2,6 +2,8 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% block registrationtab %} class="active"{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">

View File

@ -2,10 +2,11 @@ from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from .views import profile from . import views
urlpatterns = [ urlpatterns = [
url(r'^profile$', profile, name='profile'), url(r'^profile$', views.profile, name='profile'),
url(r'^logout/$', auth_views.logout, {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'), url(r'^logout/$', auth_views.logout, {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'),
url(r'^admin/participants/$', views.participants, name='participants'),
url(r'', include('django.contrib.auth.urls')), url(r'', include('django.contrib.auth.urls')),
] ]

8
accounts/utils.py Normal file
View File

@ -0,0 +1,8 @@
from django.utils.crypto import get_random_string
def enum_to_choices(enum):
return ((item.value, item.name) for item in list(enum))
def generate_user_uid():
return get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789')

View File

@ -1,8 +1,12 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.shortcuts import render from django.shortcuts import render
from django.core.exceptions import PermissionDenied
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.models import User
from .forms import ProfileForm, UserForm from .forms import ProfileForm, UserForm
from .models import Participation
@login_required @login_required
@ -20,3 +24,14 @@ def profile(request):
messages.error(request, 'Please correct those errors.') messages.error(request, 'Please correct those errors.')
return render(request, 'accounts/profile.html', {'forms': forms}) return render(request, 'accounts/profile.html', {'forms': forms})
@login_required
def participants(request):
if not request.user.is_superuser:
raise PermissionDenied()
participation_list = Participation.on_site.all()
return render(request, 'admin/participants.html', {'participation_list': participation_list})

View File

@ -1,7 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import Conversation, Message from .models import ConversationWithParticipant, ConversationAboutTalk, Message
admin.site.register(Conversation) admin.site.register(ConversationWithParticipant)
admin.site.register(ConversationAboutTalk)
admin.site.register(Message) admin.site.register(Message)

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-13 18:50 # Generated by Django 1.9.7 on 2016-06-14 19:13
from __future__ import unicode_literals from __future__ import unicode_literals
import conversations.utils import conversations.utils
@ -15,26 +15,42 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0001_initial'), ('accounts', '0001_initial'),
('contenttypes', '0002_remove_content_type_name'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Conversation', name='ConversationAboutTalk',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subscribers', models.ManyToManyField(related_name='_conversationabouttalk_subscribers_+', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ConversationWithParticipant',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Participation')), ('participation', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Participation')),
('subscribers', models.ManyToManyField(related_name='_conversation_subscribers_+', to=settings.AUTH_USER_MODEL)), ('subscribers', models.ManyToManyField(related_name='_conversationwithparticipant_subscribers_+', to=settings.AUTH_USER_MODEL)),
], ],
options={
'abstract': False,
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Message', name='Message',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('token', models.CharField(default=conversations.utils.generate_message_token, max_length=64)), ('token', models.CharField(default=conversations.utils.generate_message_token, max_length=64)),
('date', models.DateTimeField(auto_now_add=True)), ('date', models.DateTimeField(auto_now_add=True)),
('subject', models.CharField(blank=True, max_length=64)),
('content', models.TextField()), ('content', models.TextField()),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='conversations.Conversation')), ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
], ],
options={ options={
'ordering': ['date'], 'ordering': ['date'],

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-14 19:13
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

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-14 20:46
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('conversations', '0002_conversationabouttalk_talk'),
]
operations = [
migrations.RemoveField(
model_name='message',
name='subject',
),
migrations.AlterField(
model_name='conversationabouttalk',
name='subscribers',
field=models.ManyToManyField(blank=True, related_name='_conversationabouttalk_subscribers_+', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='conversationwithparticipant',
name='subscribers',
field=models.ManyToManyField(blank=True, related_name='_conversationwithparticipant_subscribers_+', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,22 +1,19 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User 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 .utils import generate_message_token, notify_by_email
from accounts.models import Participation from accounts.models import Participation
from .utils import generate_message_token from proposals.models import Talk
class Conversation(models.Model):
participation = models.OneToOneField(Participation, related_name='conversation')
subscribers = models.ManyToManyField(User, related_name='+')
def __str__(self):
return "Conversation with %s" % self.participation.user
class Message(models.Model): class Message(models.Model):
conversation = models.ForeignKey(Conversation, related_name='messages') 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) token = models.CharField(max_length=64, default=generate_message_token)
@ -29,3 +26,90 @@ class Message(models.Model):
def __str__(self): def __str__(self):
return "Message from %s" % self.author return "Message from %s" % self.author
class Conversation(models.Model):
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 get_site(self):
return self.participation.site
def __str__(self):
return "Conversation with %s" % self.participation.user
def new_message(self, message):
site = self.get_site()
subject = '[%s] Message notification' % site.name
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('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:
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 get_site(self):
return self.talk.site
def __str__(self):
return "Conversation about %s" % self.talk.title
def new_message(self, message):
site = self.get_site()
first = self.messages.first()
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-speaker', args=[message.author.username])
})
else:
if message.author not in self.subscribers.all():
self.subscribers.add(message.author)
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,86 +1,48 @@
from django.db.models.signals import post_save from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template.loader import render_to_string from django.contrib.sites.shortcuts import get_current_site
from django.core import mail from django.contrib.auth.models import User
from django.core.mail import EmailMultiAlternatives
from .models import ConversationWithParticipant, ConversationAboutTalk, Message
from .models import Conversation, Message from .utils import notify_by_email
from .utils import get_reply_addr
from proposals.models import Talk, Topic from proposals.models import Talk, Topic
from proposals.signals import new_talk
from accounts.models import Participation from accounts.models import Participation
@receiver(post_save, sender=Participation, dispatch_uid="Create Conversation") @receiver(post_save, sender=Participation, dispatch_uid="Create ConversationWithParticipant")
def create_conversation(sender, instance, created, **kwargs): def create_conversation_with_participant(sender, instance, created, **kwargs):
if not created: if not created:
return return
conversation = Conversation(participation=instance).save() conversation = ConversationWithParticipant(participation=instance)
conversation.save()
@receiver(post_save, sender=Talk, dispatch_uid="Create ConversationAboutTalk")
def create_conversation_about_talk(sender, instance, created, **kwargs):
if not created:
return
conversation = ConversationAboutTalk(talk=instance)
conversation.save()
@receiver(new_talk, dispatch_uid="Notify new talk")
def notify_new_talk(sender, instance, **kwargs):
# Subscribe reviewer for these topics to the conversation
topics = instance.topics.all()
reviewers = User.objects.filter(participation__review_topics=topics).all()
instance.conversation.subscribers.add(*reviewers)
# Notification of this new talk
message = Message(conversation=instance.conversation, author=instance.proposer,
content='The talk has been proposed.')
message.save()
@receiver(post_save, sender=Message, dispatch_uid="Notify new message") @receiver(post_save, sender=Message, dispatch_uid="Notify new message")
def notify_new_message(sender, instance, created, **kwargs): def notify_new_message(sender, instance, created, **kwargs):
if not created: if not created:
# We could send a modification notification # Possibly send a modification notification?
return return
message = instance instance.conversation.new_message(instance)
conversation = message.conversation
site = conversation.participation.site
subject = site.name
sender = message.author
if sender != conversation.participation.user \
and sender not in conversation.subscribers:
conversation.subscribers.add(sender)
dests = list(conversation.subscribers.all())
data = {
'content': message.content,
'uri': site.domain + reverse('messaging'),
}
message_id = message.token
ref = None
if conversation.messages.first().id != message.id:
ref = conversation.messages.first().token
notify_by_email(data, 'new_message', subject, sender, dests, message_id, ref)
def notify_by_email(data, template, 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/%s.txt' % template, data)
html_message = render_to_string('conversations/%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, dests, reply_to, headers in mails:
text_message, html_message = message
msg = EmailMultiAlternatives(subject, text_message, from_email, dests, 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,18 +1,20 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block messagingtab %} class="active"{% endblock %} {% block admintab %} active{% endblock %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1>Messaging</h1> <h1>Messaging</h1>
<p>You can use this page to communicate with the staff.</p> {% block heading %}
<a href="{% url 'correspondents' %}" class="btn btn-primary"><span class="glyphicon glyphicon-arrow-left"></span>&nbsp;Go back to correspondents list</a>
{% endblock %}
</div> </div>
{% for message in message_list %} {% for message in message_list %}
<div class="panel panel-default"> <div class="panel panel-{% block panelstyleblock %}default{% endblock %}">
<div class="panel-heading"> <div class="panel-heading">
{{ message.date }} | from {{ message.author.profile }} {{ message.date }} | {{ message.author.profile }}
</div> </div>
<div class="panel-body"> <div class="panel-body">
{{ message.content }} {{ message.content }}

View File

@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block admintab %} active{% endblock %}
{% block content %}
<div class="page-header">
<h1>Correspondents</h1>
This is the list of participants that you follow.
</div>
<table class="table table-striped">
<tr>
<th>#</th>
<th>Username</th>
<th>Fullname</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 '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 'correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="Unsubscribe to conversation"><span class="glyphicon glyphicon-star"></span></a>
{% else %}
<a href="{% url 'subscribe-conversation' correspondent.user.username %}?next={% url 'correspondents' %}" data-toggle="tooltip" data-placement="bottom" title="Subscribe to conversation"><span class="glyphicon glyphicon-star-empty"></span></a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -1,4 +1,4 @@
{{ content|safe }} {{ content }}
<hr> <hr>
{% if answering %} {% if answering %}

View File

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

View File

@ -0,0 +1,11 @@
Hi!<br />
<br />
A <a href="https://{{ uri }}">new talk</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 }}</p>
{% if answering %}
<hr>
Reply to this email directly to comment this talk.{% endif %}

View File

@ -0,0 +1,12 @@
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

@ -0,0 +1,10 @@
{% extends 'conversations/conversation.html' %}
{% block inboxtab %} class="active"{% endblock %}
{% block admintab %}{% endblock %}
{% block heading %}
<p>You can use this page to communicate with the staff.</p>
{% endblock %}
{% block panelstyleblock %}{% if message.author == message.conversation.participation.user %}info{% else %}success{% endif %}{% endblock %}

View File

@ -1,13 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="h1">{{ conversation }}</div>
{# for message in conversation.messages %}
<p>{{ message }}</p>
{% endfor #}
<!-- TODO -->
{% endblock %}

View File

@ -5,5 +5,9 @@ from conversations import views, emails
urlpatterns = [ urlpatterns = [
url(r'^recv/$', emails.email_recv), url(r'^recv/$', emails.email_recv),
url(r'^$', views.messaging, name='messaging'), url(r'^inbox/$', views.conversation, name='inbox'),
url(r'^$', views.correspondents, name='correspondents'),
url(r'^with/(?P<username>[\w.@+-]+)/$', views.conversation, name='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,5 +1,8 @@
from django.conf import settings from django.conf import settings
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.template.loader import render_to_string
from django.core.mail import EmailMultiAlternatives
from django.core import mail
import hashlib import hashlib
@ -29,3 +32,44 @@ def get_reply_addr(message_id, dest):
def generate_message_token(): def generate_message_token():
return get_random_string(length=60, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789') 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

@ -3,6 +3,9 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from accounts.models import Participation from accounts.models import Participation
@ -11,9 +14,18 @@ from .forms import MessageForm
@login_required @login_required
def messaging(request): def conversation(request, username=None):
participation = get_object_or_404(Participation, user=request.user, site=get_current_site(request)) if username:
if not request.user.is_superuser:
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 conversation = participation.conversation
message_list = conversation.messages.all() message_list = conversation.messages.all()
@ -23,11 +35,55 @@ def messaging(request):
message = form.save(commit=False) message = form.save(commit=False)
message.conversation = conversation message.conversation = conversation
message.author = request.user message.author = request.user
message.subject = "Assistance request from %s" % message.author.profile
message.save() message.save()
messages.success(request, 'Message sent!') messages.success(request, 'Message sent!')
return redirect('messaging') if username:
return redirect(reverse('conversation', args=[username]))
else:
return redirect('inbox')
return render(request, 'conversations/messaging.html', { return render(request, template, {
'message_list': message_list, 'message_list': message_list,
'form': form, 'form': form,
}) })
@login_required
def correspondents(request):
correspondent_list = Participation.on_site.filter(conversation__subscribers=request.user)
return render(request, 'conversations/correspondents.html', {
'correspondent_list': correspondent_list,
})
@login_required
def subscribe(request, username):
# TODO check admin
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('conversation', args=[username])
return redirect(next_url)
@login_required
def unsubscribe(request, username):
# TODO check admin
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('conversation', args=[username])
return redirect(next_url)

13
doc/reu-juin Normal file
View File

@ -0,0 +1,13 @@
responsable de track
timeline :
19 20 novembre
bâtiment A, *B* et C
mi octobre : boost logistique
mi septembre : convergence programme / sélection dorateur
fin juin / début juillet : appel à participation
juin : sponsoring
attention au chevauchement dhorraire
defraiement
planing : attention si tous les horateurs arrivent et partent en même temps, afficher quand le mec est dispo

View File

@ -53,14 +53,23 @@
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
{% if request.user.is_staff %} {% if request.user.is_staff %}
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-cog"></span>&nbsp;Administration</a></li> <li class="dropdown{% block admintab %}{% endblock %}">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-cog"></span>&nbsp;Administration&nbsp;<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li role="presentation">
<a role="menuitem" tabindex="-1" href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-dashboard"></span>&nbsp;Django Admin</a>
<a role="menuitem" tabindex="-1" href="{% url 'participants' %}"><span class="glyphicon glyphicon-user"></span>&nbsp;Participants</a>
<a role="menuitem" tabindex="-1" href="{% url 'correspondents' %}"><span class="glyphicon glyphicon-envelope"></span>&nbsp;Correspondence</a>
</li>
</ul>
</li>
{% endif %} {% endif %}
<li{% block messagingtab %}{% endblock %}><a href="{% url 'messaging' %}" data-toggle="tooltip" data-placement="bottom" title="Messaging"><span class="glyphicon glyphicon-envelope"></span> Messaging</a></li> <li{% block inboxtab %}{% endblock %}><a href="{% url 'inbox' %}" data-toggle="tooltip" data-placement="bottom" title="Inbox"><span class="glyphicon glyphicon-envelope"></span>&nbsp;Inbox</a></li>
<li{% block profiletab %}{% endblock %}><a href="{% url 'profile' %}" data-toggle="tooltip" data-placement="bottom" title="Profile"><span class="glyphicon glyphicon-user"></span> {{ request.user.username }}</a></li> <li{% block profiletab %}{% endblock %}><a href="{% url 'profile' %}" data-toggle="tooltip" data-placement="bottom" title="Profile"><span class="glyphicon glyphicon-user"></span>&nbsp;{{ request.user.username }}</a></li>
<li><a href="{% url 'logout' %}" data-toggle="tooltip" data-placement="bottom" title="Logout"><span class="glyphicon glyphicon-log-out"></span></a></li> <li><a href="{% url 'logout' %}" data-toggle="tooltip" data-placement="bottom" title="Logout"><span class="glyphicon glyphicon-log-out"></span></a></li>
{% else %} {% else %}
<li><a href="{% url 'login' %}?next={{ request.path }}"><span class="glyphicon glyphicon-log-in"></span>&nbsp;Login</a></li> <li{% block registrationtab %}{% endblock %}><a href="{% url 'registration_register' %}"><span class="glyphicon glyphicon-edit"></span>&nbsp;Register</a></li>
<li><a href="{% url 'registration_register' %}"><span class="glyphicon glyphicon-edit"></span>&nbsp;Register</a></li> <li{% block logintab %}{% endblock %}><a href="{% url 'login' %}?next={{ request.path }}"><span class="glyphicon glyphicon-log-in"></span>&nbsp;Login</a></li>
{% endif %} {% endif %}
</ul> </ul>
{% block navbar-right %}{% endblock %} {% block navbar-right %}{% endblock %}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-13 18:50 # Generated by Django 1.9.7 on 2016-06-14 19:13
from __future__ import unicode_literals from __future__ import unicode_literals
import autoslug.fields import autoslug.fields
@ -15,8 +15,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('sites', '0002_alter_domain_unique'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sites', '0002_alter_domain_unique'),
] ]
operations = [ operations = [
@ -39,6 +39,7 @@ class Migration(migrations.Migration):
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)), ('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
('description', models.TextField(blank=True, verbose_name='Description')), ('description', models.TextField(blank=True, verbose_name='Description')),
('event', models.IntegerField(choices=[(1, 'conference'), (2, 'workshop'), (3, 'stand'), (4, 'other')], default=1)), ('event', models.IntegerField(choices=[(1, 'conference'), (2, 'workshop'), (3, 'stand'), (4, 'other')], default=1)),
('proposer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')), ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
('speakers', models.ManyToManyField(through='proposals.Speech', to=settings.AUTH_USER_MODEL)), ('speakers', models.ManyToManyField(through='proposals.Speech', to=settings.AUTH_USER_MODEL)),
], ],
@ -67,6 +68,6 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='speech', name='speech',
unique_together=set([('speaker', 'talk'), ('order', 'talk')]), unique_together=set([('order', 'talk'), ('speaker', 'talk')]),
), ),
] ]

View File

@ -8,7 +8,8 @@ from django.db import models
from autoslug import AutoSlugField from autoslug import AutoSlugField
from accounts.models import enum_to_choices from accounts.utils import enum_to_choices
__all__ = ['Topic', 'Talk', 'Speech'] __all__ = ['Topic', 'Talk', 'Speech']
@ -31,6 +32,7 @@ class Talk(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE) site = models.ForeignKey(Site, on_delete=models.CASCADE)
proposer = models.ForeignKey(User, related_name='+')
speakers = models.ManyToManyField(User, through='Speech') speakers = models.ManyToManyField(User, through='Speech')
title = models.CharField(max_length=128, verbose_name='Title') title = models.CharField(max_length=128, verbose_name='Title')
slug = AutoSlugField(populate_from='title', unique=True) slug = AutoSlugField(populate_from='title', unique=True)
@ -47,6 +49,19 @@ class Talk(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('show-talk', kwargs={'slug': self.slug}) return reverse('show-talk', kwargs={'slug': self.slug})
def is_editable_by(self, user):
if user.is_superuser:
return True
if user == self.proposer:
return True
if user in talk.speakers.all():
return True
try:
participation = Participation.on_site.get(user=user)
except Participation.DoesNotExists:
return False
return self.topics.filter(pk=participation.review_topics.pk).exists()
class Speech(models.Model): class Speech(models.Model):

4
proposals/signals.py Normal file
View File

@ -0,0 +1,4 @@
from django.dispatch import Signal
new_talk = Signal(providing_args=["sender", "instance"])

View File

@ -6,7 +6,9 @@
<h1>{{ talk.title }}</h1> <h1>{{ talk.title }}</h1>
{% if edit_perm %}
<a class="btn btn-primary" href="{% url 'edit-talk' talk.slug %}">edit</a><br /> <a class="btn btn-primary" href="{% url 'edit-talk' talk.slug %}">edit</a><br />
{% endif %}
<p>{{ talk.get_event_display }}</p> <p>{{ talk.get_event_display }}</p>

View File

@ -9,6 +9,7 @@ from django.views.generic import DetailView, ListView
from proposals.forms import TalkForm from proposals.forms import TalkForm
from proposals.models import Speech, Talk, Topic from proposals.models import Speech, Talk, Topic
from .signals import new_talk
def home(request): def home(request):
@ -47,8 +48,7 @@ def talk_edit(request, talk=None):
talk = get_object_or_404(Talk, slug=talk) talk = get_object_or_404(Talk, slug=talk)
if talk.site != get_current_site(request): if talk.site != get_current_site(request):
raise PermissionDenied() raise PermissionDenied()
if not request.user.is_superuser and request.user not in talk.speakers.all(): if not talk.has_perm(request.user):
# FIXME fine permissions
raise PermissionDenied() raise PermissionDenied()
form = TalkForm(request.POST or None, instance=talk) form = TalkForm(request.POST or None, instance=talk)
if request.method == 'POST' and form.is_valid(): if request.method == 'POST' and form.is_valid():
@ -56,12 +56,13 @@ def talk_edit(request, talk=None):
talk = form.save() talk = form.save()
messages.success(request, 'Talk modified successfully!') messages.success(request, 'Talk modified successfully!')
else: else:
site = get_current_site(request)
talk = form.save(commit=False) talk = form.save(commit=False)
talk.site = site talk.site = get_current_site(request)
talk.proposer = request.user
talk.save() talk.save()
form.save_m2m() form.save_m2m()
Speech.objects.create(speaker=request.user, talk=talk) Speech.objects.create(speaker=request.user, talk=talk)
new_talk.send(talk.__class__, instance=talk)
messages.success(request, 'Talk proposed successfully!') messages.success(request, 'Talk proposed successfully!')
return redirect(talk.get_absolute_url()) return redirect(talk.get_absolute_url())
return render(request, 'proposals/talk_edit.html', { return render(request, 'proposals/talk_edit.html', {
@ -71,6 +72,10 @@ def talk_edit(request, talk=None):
class TalkDetail(LoginRequiredMixin, DetailView): class TalkDetail(LoginRequiredMixin, DetailView):
queryset = Talk.on_site.all() queryset = Talk.on_site.all()
def get_context_data(self, **kwargs):
context = super(TalkDetail, self).get_context_data(**kwargs)
context['edit_perm'] = self.object.is_editable_by(self.request.user)
return context
class TopicList(LoginRequiredMixin, ListView): class TopicList(LoginRequiredMixin, ListView):