diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
index 37b9946..56cb873 100644
--- a/accounts/migrations/0001_initial.py
+++ b/accounts/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.9.7 on 2016-06-12 15:10
+# Generated by Django 1.9.7 on 2016-06-12 21:41
from __future__ import unicode_literals
from django.conf import settings
@@ -14,8 +14,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('sites', '0002_alter_domain_unique'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
('departure', models.DateTimeField(blank=True, 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)),
- ('constraints', models.TextField()),
+ ('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)),
],
diff --git a/accounts/models.py b/accounts/models.py
index 804d9bc..db4ad50 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -37,7 +37,7 @@ class Speaker(models.Model):
departure = models.DateTimeField(blank=True, null=True)
transport = models.IntegerField(choices=enum_to_choices(TRANSPORTS), blank=True, null=True)
connector = models.IntegerField(choices=enum_to_choices(CONNECTORS), blank=True, null=True)
- constraints = models.TextField()
+ constraints = models.TextField(blank=True)
objects = models.Manager()
on_site = CurrentSiteManager()
diff --git a/accounts/templates/registration/login.html b/accounts/templates/registration/login.html
index fdc5adc..e95f123 100644
--- a/accounts/templates/registration/login.html
+++ b/accounts/templates/registration/login.html
@@ -28,7 +28,7 @@
- You do not have an account? Please
register.
+ You do not have an account yet? Please
register.
diff --git a/conversations/__init__.py b/conversations/__init__.py
new file mode 100644
index 0000000..113bdeb
--- /dev/null
+++ b/conversations/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'conversations.apps.ConversationsConfig'
diff --git a/conversations/admin.py b/conversations/admin.py
new file mode 100644
index 0000000..d12718a
--- /dev/null
+++ b/conversations/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+
+from .models import Conversation, Message
+
+
+admin.site.register(Conversation)
+admin.site.register(Message)
diff --git a/conversations/apps.py b/conversations/apps.py
new file mode 100644
index 0000000..73892cf
--- /dev/null
+++ b/conversations/apps.py
@@ -0,0 +1,8 @@
+from django.apps import AppConfig
+
+
+class ConversationsConfig(AppConfig):
+ name = 'conversations'
+
+ def ready(self):
+ import conversations.signals
diff --git a/conversations/migrations/0001_initial.py b/conversations/migrations/0001_initial.py
new file mode 100644
index 0000000..7dff8b4
--- /dev/null
+++ b/conversations/migrations/0001_initial.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-06-12 21:41
+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),
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Conversation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('speaker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Speaker')),
+ ('subscribers', models.ManyToManyField(related_name='_conversation_subscribers_+', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Message',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('token', models.CharField(max_length=64)),
+ ('date', models.DateTimeField(auto_now_add=True)),
+ ('content', models.TextField()),
+ ('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')),
+ ],
+ options={
+ 'ordering': ['date'],
+ },
+ ),
+ ]
diff --git a/conversations/migrations/__init__.py b/conversations/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/conversations/models.py b/conversations/models.py
new file mode 100644
index 0000000..1ee2f04
--- /dev/null
+++ b/conversations/models.py
@@ -0,0 +1,29 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+from accounts.models import Speaker
+
+
+class Conversation(models.Model):
+
+ speaker = models.ForeignKey(Speaker, related_name='conversation')
+ subscribers = models.ManyToManyField(User, related_name='+')
+
+ def __str__(self):
+ return "Conversation with %s" % self.speaker
+
+
+class Message(models.Model):
+
+ token = models.CharField(max_length=64)
+ conversation = models.ForeignKey(Conversation, related_name='messages')
+
+ author = models.ForeignKey(User)
+ date = models.DateTimeField(auto_now_add=True)
+ content = models.TextField()
+
+ class Meta:
+ ordering = ['date']
+
+ def __str__(self):
+ return "Message from %s" % self.author
diff --git a/conversations/signals.py b/conversations/signals.py
new file mode 100644
index 0000000..c46beb3
--- /dev/null
+++ b/conversations/signals.py
@@ -0,0 +1,77 @@
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.template.loader import render_to_string
+from django.core import mail
+from django.core.mail import EmailMultiAlternatives
+
+
+from .models import Message
+from .utils import get_reply_addr
+from proposals.models import Talk, Topic
+
+
+@receiver(post_save, sender=Message, dispatch_uid="Notify new message")
+def notify_new_message(sender, instance, created, **kwargs):
+ if not created:
+ # We could send a modification notification
+ return
+ message = instance
+ conversation = message.conversation
+ site = conversation.speaker.site
+ subject = site.name
+ sender = instance.author
+ dests = list(conversation.subscribers.all())
+ if conversation.speaker.user not in dests:
+ dests += [conversation.speaker.user]
+ data = {
+ 'content': message.content,
+ 'uri': site.domain + reverse('show-conversation', args=[conversation.id]),
+ }
+ 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'):
+ 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)
diff --git a/conversations/templates/conversations/new_message.html b/conversations/templates/conversations/new_message.html
new file mode 100644
index 0000000..7f261f4
--- /dev/null
+++ b/conversations/templates/conversations/new_message.html
@@ -0,0 +1,8 @@
+{{ content|safe }}
+
+
+{% if answering %}
+Reply to this email directly or view it online.
+{% else %}
+Reply online.
+{% endif %}
diff --git a/conversations/templates/conversations/new_message.txt b/conversations/templates/conversations/new_message.txt
new file mode 100644
index 0000000..133a36c
--- /dev/null
+++ b/conversations/templates/conversations/new_message.txt
@@ -0,0 +1,4 @@
+{{ content|safe }}
+
+--
+Reply {% if answering %}to this email directly or view it {% endif %}online: {{ uri }}
diff --git a/conversations/tests.py b/conversations/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/conversations/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/conversations/urls.py b/conversations/urls.py
new file mode 100644
index 0000000..a6ca3d3
--- /dev/null
+++ b/conversations/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls import url
+
+from conversations import views
+
+
+urlpatterns = [
+ url(r'^(?P[0-9]+)$', views.conversation_details, name='show-conversation'),
+]
diff --git a/conversations/utils.py b/conversations/utils.py
new file mode 100644
index 0000000..e00ba87
--- /dev/null
+++ b/conversations/utils.py
@@ -0,0 +1,26 @@
+from django.conf import settings
+
+import hashlib
+
+
+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)
+
+ return ['%s+%10s%s%10s%s' % (name, dest.pk, message_id, key, domain)]
diff --git a/conversations/views.py b/conversations/views.py
new file mode 100644
index 0000000..ffc0930
--- /dev/null
+++ b/conversations/views.py
@@ -0,0 +1,13 @@
+from django.shortcuts import render
+from django.shortcuts import get_object_or_404, redirect, render
+from django.contrib.sites.shortcuts import get_current_site
+
+from .models import Message
+
+
+def conversation_details(request, conversation):
+ conversation = get_object_or_404(Conversation,
+ id=conversation, speaker__site=get_current_site(request))
+ return render(request, 'conversations/message.html', {
+ 'messages': conversation.messages,
+ })
diff --git a/ponyconf/settings.py b/ponyconf/settings.py
index bd1d33f..9a8b33c 100644
--- a/ponyconf/settings.py
+++ b/ponyconf/settings.py
@@ -31,8 +31,7 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
- 'accounts',
- 'registration',
+ # build-in apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -41,11 +40,16 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.sites',
+ # external apps
'djangobower',
'bootstrap3',
+ 'registration',
+ # our apps
+ 'accounts',
'ponyconf',
'proposals',
+ 'conversations',
]
MIDDLEWARE_CLASSES = [
diff --git a/ponyconf/urls.py b/ponyconf/urls.py
index a29dba5..ad1d5b4 100644
--- a/ponyconf/urls.py
+++ b/ponyconf/urls.py
@@ -21,4 +21,5 @@ urlpatterns = [
url(r'^accounts/', include('accounts.urls')),
url(r'^registration/', include('registration.backends.default.urls')),
url(r'^', include('proposals.urls')),
+ url(r'^conversations/', include('conversations.urls')),
]
diff --git a/proposals/admin.py b/proposals/admin.py
index e5a36bf..fa94429 100644
--- a/proposals/admin.py
+++ b/proposals/admin.py
@@ -1,7 +1,7 @@
from django.contrib import admin
-from proposals.models import Speach, Talk, Topic
+from proposals.models import Speech, Talk, Topic
admin.site.register(Topic)
admin.site.register(Talk)
-admin.site.register(Speach)
+admin.site.register(Speech)
diff --git a/proposals/migrations/0001_initial.py b/proposals/migrations/0001_initial.py
index 927a56a..056070a 100644
--- a/proposals/migrations/0001_initial.py
+++ b/proposals/migrations/0001_initial.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Generated by Django 1.9.7 on 2016-06-12 16:42
+# Generated by Django 1.9.7 on 2016-06-12 21:41
from __future__ import unicode_literals
import autoslug.fields
@@ -21,7 +21,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='Speach',
+ name='Speech',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6'), (7, '7')], default=1)),
@@ -38,8 +38,9 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=128, verbose_name='Title')),
('slug', autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique=True)),
('description', models.TextField(blank=True, verbose_name='Description')),
+ ('event', models.IntegerField(choices=[(1, 'conference'), (2, 'workshop'), (3, 'stand'), (4, 'other')], default=1)),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.Site')),
- ('speakers', models.ManyToManyField(through='proposals.Speach', to=settings.AUTH_USER_MODEL)),
+ ('speakers', models.ManyToManyField(through='proposals.Speech', to=settings.AUTH_USER_MODEL)),
],
managers=[
('objects', django.db.models.manager.Manager()),
@@ -60,12 +61,12 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(blank=True, to='proposals.Topic'),
),
migrations.AddField(
- model_name='speach',
+ model_name='speech',
name='talk',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.Talk'),
),
migrations.AlterUniqueTogether(
- name='speach',
+ name='speech',
unique_together=set([('order', 'talk'), ('speaker', 'talk')]),
),
]
diff --git a/proposals/migrations/0002_talk_event.py b/proposals/migrations/0002_talk_event.py
deleted file mode 100644
index 9bf800f..0000000
--- a/proposals/migrations/0002_talk_event.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.7 on 2016-06-12 20:26
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('proposals', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='talk',
- name='event',
- field=models.IntegerField(choices=[(1, 'conference'), (2, 'workshop'), (3, 'stand'), (4, 'other')], default=1),
- ),
- ]
diff --git a/proposals/models.py b/proposals/models.py
index ddab9d3..bfaa2de 100644
--- a/proposals/models.py
+++ b/proposals/models.py
@@ -10,7 +10,7 @@ from autoslug import AutoSlugField
from accounts.models import enum_to_choices
-__all__ = ['Topic', 'Talk', 'Speach']
+__all__ = ['Topic', 'Talk', 'Speech']
class Topic(models.Model):
@@ -31,7 +31,7 @@ class Talk(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
- speakers = models.ManyToManyField(User, through='Speach')
+ speakers = models.ManyToManyField(User, through='Speech')
title = models.CharField(max_length=128, verbose_name='Title')
slug = AutoSlugField(populate_from='title', unique=True)
description = models.TextField(blank=True, verbose_name='Description')
@@ -48,7 +48,7 @@ class Talk(models.Model):
return reverse('show-talk', kwargs={'slug': self.slug})
-class Speach(models.Model):
+class Speech(models.Model):
SPEAKER_NO = tuple((i, str(i)) for i in range(1, 8))
diff --git a/proposals/tests.py b/proposals/tests.py
index 954e416..3af5e6d 100644
--- a/proposals/tests.py
+++ b/proposals/tests.py
@@ -2,7 +2,7 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase
-from .models import Talk, Topic, Speach
+from .models import Talk, Topic, Speech
class ProposalsTests(TestCase):
@@ -36,7 +36,7 @@ class ProposalsTests(TestCase):
self.assertEqual(self.client.get(reverse('list-talks')).status_code, 200)
# Models str & get_asbolute_url
- for model in [Talk, Topic, Speach]:
+ for model in [Talk, Topic, Speech]:
item = model.objects.first()
self.assertEqual(self.client.get(item.get_absolute_url()).status_code, 200)
self.assertTrue(str(item))
diff --git a/proposals/views.py b/proposals/views.py
index 4f6b851..025e96a 100644
--- a/proposals/views.py
+++ b/proposals/views.py
@@ -9,7 +9,7 @@ from django.views.generic import DetailView, ListView
from accounts.models import Speaker
from proposals.forms import TalkForm
-from proposals.models import Speach, Talk, Topic
+from proposals.models import Speech, Talk, Topic
def home(request):
@@ -63,7 +63,7 @@ def talk_edit(request, talk=None):
talk.save()
form.save_m2m()
Speaker.on_site.get_or_create(user=request.user, site=site)
- Speach.objects.create(speaker=request.user, talk=talk)
+ Speech.objects.create(speaker=request.user, talk=talk)
messages.success(request, 'Talk proposed successfully!')
return redirect(talk.get_absolute_url())
return render(request, 'proposals/talk_edit.html', {
@@ -80,7 +80,7 @@ class TopicList(LoginRequiredMixin, ListView):
class SpeakerList(LoginRequiredMixin, ListView):
- queryset = User.objects.filter(speach__talk=Talk.on_site.all())
+ queryset = User.objects.filter(speech__talk=Talk.on_site.all())
template_name = 'proposals/speaker_list.html'