conversation app (continuation)
This commit is contained in:
parent
e4f413adcc
commit
d62a494d9f
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from accounts.models import Profile, Speaker
|
from accounts.models import Profile, Participation
|
||||||
|
|
||||||
admin.site.register(Profile) # FIXME extend user admin
|
admin.site.register(Profile) # FIXME extend user admin
|
||||||
admin.site.register(Speaker)
|
admin.site.register(Participation)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.9.7 on 2016-06-12 21:41
|
# Generated by Django 1.9.7 on 2016-06-13 18:50
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import accounts.models
|
||||||
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
|
||||||
|
@ -14,21 +15,13 @@ 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 = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Profile',
|
name='Participation',
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('biography', models.TextField(blank=True, verbose_name='Biography')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Speaker',
|
|
||||||
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')),
|
||||||
('arrival', models.DateTimeField(blank=True, null=True)),
|
('arrival', models.DateTimeField(blank=True, null=True)),
|
||||||
|
@ -44,8 +37,17 @@ class Migration(migrations.Migration):
|
||||||
('on_site', django.contrib.sites.managers.CurrentSiteManager()),
|
('on_site', django.contrib.sites.managers.CurrentSiteManager()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Profile',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('biography', models.TextField(blank=True, verbose_name='Biography')),
|
||||||
|
('email_token', models.CharField(default=accounts.models.generate_user_uid, max_length=12)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='speaker',
|
name='participation',
|
||||||
unique_together=set([('site', 'user')]),
|
unique_together=set([('site', 'user')]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,18 +5,23 @@ 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', 'Speaker']
|
__all__ = ['Profile', 'Participation']
|
||||||
|
|
||||||
|
|
||||||
def enum_to_choices(enum):
|
def enum_to_choices(enum):
|
||||||
return ((item.value, item.name) for item in list(enum))
|
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):
|
||||||
|
|
||||||
user = models.OneToOneField(User)
|
user = models.OneToOneField(User)
|
||||||
biography = models.TextField(blank=True, verbose_name='Biography')
|
biography = models.TextField(blank=True, verbose_name='Biography')
|
||||||
|
email_token = models.CharField(max_length=12, default=generate_user_uid)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user.get_full_name() or self.user.username
|
return self.user.get_full_name() or self.user.username
|
||||||
|
@ -25,16 +30,17 @@ class Profile(models.Model):
|
||||||
return reverse('profile')
|
return reverse('profile')
|
||||||
|
|
||||||
|
|
||||||
class Speaker(models.Model):
|
class Participation(models.Model):
|
||||||
|
|
||||||
TRANSPORTS = IntEnum('Transport', 'train plane')
|
TRANSPORTS = IntEnum('Transport', 'train plane')
|
||||||
CONNECTORS = IntEnum('Connector', 'VGA HDMI miniDP')
|
CONNECTORS = IntEnum('Connector', 'VGA HDMI miniDP')
|
||||||
|
|
||||||
site = models.ForeignKey(Site, on_delete=models.CASCADE)
|
site = models.ForeignKey(Site, on_delete=models.CASCADE)
|
||||||
|
|
||||||
user = models.ForeignKey(User)
|
user = models.ForeignKey(User)
|
||||||
|
|
||||||
arrival = models.DateTimeField(blank=True, null=True)
|
arrival = models.DateTimeField(blank=True, null=True)
|
||||||
departure = models.DateTimeField(blank=True, null=True)
|
departure = models.DateTimeField(blank=True, null=True)
|
||||||
|
# TODO: These should multi-choice fields
|
||||||
transport = models.IntegerField(choices=enum_to_choices(TRANSPORTS), 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)
|
connector = models.IntegerField(choices=enum_to_choices(CONNECTORS), blank=True, null=True)
|
||||||
constraints = models.TextField(blank=True)
|
constraints = models.TextField(blank=True)
|
||||||
|
@ -43,13 +49,14 @@ class Speaker(models.Model):
|
||||||
on_site = CurrentSiteManager()
|
on_site = CurrentSiteManager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
# A User can participe only once to a Conference (= Site)
|
||||||
unique_together = ('site', 'user')
|
unique_together = ('site', 'user')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.user.profile)
|
return "%s participation to %s" % (str(self.user.profile), self.site.name)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('show-speaker', kwargs={'username': self.user.username})
|
return reverse('show-participation', kwargs={'username': self.user.username})
|
||||||
|
|
||||||
|
|
||||||
def create_profile(sender, instance, created, **kwargs):
|
def create_profile(sender, instance, created, **kwargs):
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
|
||||||
|
from .models import Participation
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_in)
|
@receiver(user_logged_in)
|
||||||
def on_user_logged_in(sender, request, **kwargs):
|
def on_user_logged_in(sender, request, **kwargs):
|
||||||
|
Participation.on_site.get_or_create(user=request.user, site=get_current_site(request))
|
||||||
messages.success(request, 'Welcome!', fail_silently=True) # FIXME
|
messages.success(request, 'Welcome!', fail_silently=True) # FIXME
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib.sites.models import Site
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import Profile, Speaker
|
from .models import Profile, Participation
|
||||||
|
|
||||||
ROOT_URL = 'accounts'
|
ROOT_URL = 'accounts'
|
||||||
|
|
||||||
|
@ -12,12 +12,12 @@ class AccountTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for guy in 'ab':
|
for guy in 'ab':
|
||||||
User.objects.create_user(guy, email='%s@example.org' % guy, password=guy)
|
User.objects.create_user(guy, email='%s@example.org' % guy, password=guy)
|
||||||
Speaker.objects.create(user=User.objects.first(), site=Site.objects.first())
|
Participation.objects.create(user=User.objects.first(), site=Site.objects.first())
|
||||||
|
|
||||||
def test_models(self):
|
def test_models(self):
|
||||||
self.assertEqual(Profile.objects.count(), 2)
|
self.assertEqual(Profile.objects.count(), 2)
|
||||||
self.client.login(username='b', password='b')
|
self.client.login(username='b', password='b')
|
||||||
for model in [Profile, Speaker]:
|
for model in [Profile, Participation]:
|
||||||
item = model.objects.first()
|
item = model.objects.first()
|
||||||
self.assertEqual(self.client.get(item.get_absolute_url()).status_code, 200)
|
self.assertEqual(self.client.get(item.get_absolute_url()).status_code, 200)
|
||||||
self.assertTrue(str(item))
|
self.assertTrue(str(item))
|
||||||
|
|
|
@ -2,5 +2,6 @@ from django.contrib import admin
|
||||||
|
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Conversation)
|
admin.site.register(Conversation)
|
||||||
admin.site.register(Message)
|
admin.site.register(Message)
|
||||||
|
|
|
@ -5,4 +5,4 @@ class ConversationsConfig(AppConfig):
|
||||||
name = 'conversations'
|
name = 'conversations'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import conversations.signals # noqa
|
import conversations.signals
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.http import Http404
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.mail import mail_admins
|
||||||
|
|
||||||
|
from .utils import hexdigest_sha256
|
||||||
|
from .models import Message
|
||||||
|
|
||||||
|
import email
|
||||||
|
import re
|
||||||
|
from sys import version_info as python_version
|
||||||
|
|
||||||
|
|
||||||
|
@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')
|
||||||
|
if key != settings.REPLY_KEY:
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
if 'email' not in request.FILES:
|
||||||
|
raise HttpResponse(status=400) # Bad Request
|
||||||
|
|
||||||
|
msg = request.FILES['email']
|
||||||
|
if python_version < (3,):
|
||||||
|
msg = email.message_from_file(msg)
|
||||||
|
else:
|
||||||
|
msg = email.message_from_bytes(msg.read())
|
||||||
|
|
||||||
|
mfrom = msg.get('From')
|
||||||
|
mto = msg.get('To')
|
||||||
|
subject = msg.get('Subject')
|
||||||
|
|
||||||
|
if msg.is_multipart():
|
||||||
|
msgs = msg.get_payload()
|
||||||
|
for m in msgs:
|
||||||
|
if m.get_content_type == 'text/plain':
|
||||||
|
content = m.get_payload(decode=True)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
content = msgs[0].get_payload(decode=True)
|
||||||
|
else:
|
||||||
|
content = msg.get_payload(decode=True)
|
||||||
|
|
||||||
|
if python_version < (3,):
|
||||||
|
content = content.decode('utf-8')
|
||||||
|
|
||||||
|
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
|
||||||
|
for _mto in map(lambda x: x.strip(), mto.split(',')):
|
||||||
|
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()
|
|
@ -0,0 +1,10 @@
|
||||||
|
from django.forms.models import modelform_factory
|
||||||
|
|
||||||
|
from .models import Message
|
||||||
|
|
||||||
|
|
||||||
|
MessageForm = modelform_factory(Message,
|
||||||
|
fields=['content'])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.9.7 on 2016-06-12 21:41
|
# Generated by Django 1.9.7 on 2016-06-13 18:50
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import conversations.utils
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
@ -21,7 +22,7 @@ class Migration(migrations.Migration):
|
||||||
name='Conversation',
|
name='Conversation',
|
||||||
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')),
|
||||||
('speaker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversation', to='accounts.Speaker')),
|
('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='_conversation_subscribers_+', to=settings.AUTH_USER_MODEL)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -29,7 +30,7 @@ class Migration(migrations.Migration):
|
||||||
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')),
|
||||||
('token', models.CharField(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)),
|
||||||
('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)),
|
||||||
|
|
|
@ -1,27 +1,25 @@
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from accounts.models import Speaker
|
from accounts.models import Participation
|
||||||
|
from .utils import generate_message_token
|
||||||
|
|
||||||
|
|
||||||
class Conversation(models.Model):
|
class Conversation(models.Model):
|
||||||
|
|
||||||
speaker = models.ForeignKey(Speaker, related_name='conversation')
|
participation = models.OneToOneField(Participation, related_name='conversation')
|
||||||
subscribers = models.ManyToManyField(User, related_name='+')
|
subscribers = models.ManyToManyField(User, related_name='+')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Conversation with %s" % self.speaker
|
return "Conversation with %s" % self.participation.user
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('show-conversation', kwargs={'conversation': self.pk})
|
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
|
|
||||||
token = models.CharField(max_length=64)
|
|
||||||
conversation = models.ForeignKey(Conversation, related_name='messages')
|
conversation = models.ForeignKey(Conversation, related_name='messages')
|
||||||
|
|
||||||
|
token = models.CharField(max_length=64, default=generate_message_token)
|
||||||
|
|
||||||
author = models.ForeignKey(User)
|
author = models.ForeignKey(User)
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
content = models.TextField()
|
content = models.TextField()
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: %s KEY@URL" % sys.argv[0])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
key, url = sys.argv[1].split('@')
|
||||||
|
|
||||||
|
email = sys.stdin.buffer.raw.read()
|
||||||
|
sys.stdout.buffer.write(email) # DO NOT REMOVE
|
||||||
|
|
||||||
|
requests.post(
|
||||||
|
url,
|
||||||
|
data={
|
||||||
|
'key': key,
|
||||||
|
},
|
||||||
|
files={
|
||||||
|
'email': ('email.txt', email),
|
||||||
|
}
|
||||||
|
)
|
|
@ -1,13 +1,23 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core import mail
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
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.template.loader import render_to_string
|
||||||
|
from django.core import mail
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
|
||||||
from .models import Message
|
|
||||||
|
from .models import Conversation, Message
|
||||||
from .utils import get_reply_addr
|
from .utils import get_reply_addr
|
||||||
|
from proposals.models import Talk, Topic
|
||||||
|
from accounts.models import Participation
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Participation, dispatch_uid="Create Conversation")
|
||||||
|
def create_conversation(sender, instance, created, **kwargs):
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
conversation = Conversation(participation=instance).save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Message, dispatch_uid="Notify new message")
|
@receiver(post_save, sender=Message, dispatch_uid="Notify new message")
|
||||||
|
@ -17,15 +27,16 @@ def notify_new_message(sender, instance, created, **kwargs):
|
||||||
return
|
return
|
||||||
message = instance
|
message = instance
|
||||||
conversation = message.conversation
|
conversation = message.conversation
|
||||||
site = conversation.speaker.site
|
site = conversation.participation.site
|
||||||
subject = site.name
|
subject = site.name
|
||||||
sender = instance.author
|
sender = message.author
|
||||||
|
if sender != conversation.participation.user \
|
||||||
|
and sender not in conversation.subscribers:
|
||||||
|
conversation.subscribers.add(sender)
|
||||||
dests = list(conversation.subscribers.all())
|
dests = list(conversation.subscribers.all())
|
||||||
if conversation.speaker.user not in dests:
|
|
||||||
dests += [conversation.speaker.user]
|
|
||||||
data = {
|
data = {
|
||||||
'content': message.content,
|
'content': message.content,
|
||||||
'uri': site.domain + reverse('show-conversation', args=[conversation.id]),
|
'uri': site.domain + reverse('messaging'),
|
||||||
}
|
}
|
||||||
message_id = message.token
|
message_id = message.token
|
||||||
ref = None
|
ref = None
|
||||||
|
@ -36,7 +47,7 @@ def notify_new_message(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
def notify_by_email(data, template, subject, sender, dests, message_id, ref=None):
|
def notify_by_email(data, template, subject, sender, dests, message_id, ref=None):
|
||||||
|
|
||||||
if hasattr(settings, 'REPLY_EMAIL'):
|
if hasattr(settings, 'REPLY_EMAIL') and hasattr(settings, 'REPLY_KEY'):
|
||||||
data.update({'answering': True})
|
data.update({'answering': True})
|
||||||
|
|
||||||
text_message = render_to_string('conversations/%s.txt' % template, data)
|
text_message = render_to_string('conversations/%s.txt' % template, data)
|
||||||
|
@ -47,9 +58,9 @@ def notify_by_email(data, template, subject, sender, dests, message_id, ref=None
|
||||||
email=settings.DEFAULT_FROM_EMAIL)
|
email=settings.DEFAULT_FROM_EMAIL)
|
||||||
|
|
||||||
# Generating headers
|
# Generating headers
|
||||||
headers = {
|
headers = {
|
||||||
'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL),
|
'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL),
|
||||||
}
|
}
|
||||||
if ref:
|
if ref:
|
||||||
# This email reference a previous one
|
# This email reference a previous one
|
||||||
headers.update({
|
headers.update({
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block messagingtab %} class="active"{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Messaging</h1>
|
||||||
|
<p>You can use this page to communicate with the staff.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for message in message_list %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
{{ message.date }} | from {{ message.author.profile }}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Send a message
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form action="" method="post" role="form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea class="form-control" name="content" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-envelope"></span> Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
{% if answering %}
|
{% if answering %}
|
||||||
Reply to this email directly or <a href="{{ uri }}">view it online</a>.
|
Reply to this email directly or <a href="https://{{ uri }}">view it online</a>.
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ uri }}">Reply online</a>.
|
<a href="https://{{ uri }}">Reply online</a>.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{{ content|safe }}
|
{{ content|safe }}
|
||||||
|
|
||||||
--
|
--
|
||||||
Reply {% if answering %}to this email directly or view it {% endif %}online: {{ uri }}
|
Reply {% if answering %}to this email directly or view it {% endif %}online: https://{{ uri }}
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from accounts.models import Speaker
|
from accounts.models import Participation
|
||||||
|
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
from .utils import get_reply_addr
|
from .utils import get_reply_addr
|
||||||
|
@ -11,7 +11,7 @@ from .utils import get_reply_addr
|
||||||
class ConversationTests(TestCase):
|
class ConversationTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
a, b = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'ab')
|
a, b = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'ab')
|
||||||
speaker = Speaker.objects.create(user=a, site=Site.objects.first())
|
participation = Participation.objects.create(user=a, site=Site.objects.first())
|
||||||
conversation = Conversation.objects.create(speaker=speaker)
|
conversation = Conversation.objects.create(speaker=speaker)
|
||||||
Message.objects.create(token='pipo', conversation=conversation, author=a, content='allo')
|
Message.objects.create(token='pipo', conversation=conversation, author=a, content='allo')
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from conversations import views
|
from conversations import views, emails
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^(?P<conversation>[0-9]+)$', views.conversation_details, name='show-conversation'),
|
url(r'^recv/$', emails.email_recv),
|
||||||
|
url(r'^$', views.messaging, name='messaging'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
def hexdigest_sha256(*args):
|
def hexdigest_sha256(*args):
|
||||||
|
@ -21,6 +22,10 @@ def get_reply_addr(message_id, dest):
|
||||||
pos = addr.find('@')
|
pos = addr.find('@')
|
||||||
name = addr[:pos]
|
name = addr[:pos]
|
||||||
domain = addr[pos:]
|
domain = addr[pos:]
|
||||||
key = hexdigest_sha256(settings.SECRET_KEY, message_id, dest.pk)
|
key = hexdigest_sha256(settings.SECRET_KEY, message_id, dest.pk)[0:12]
|
||||||
|
|
||||||
return ['%s+%10s%s%10s%s' % (name, dest.pk, message_id, key, domain)]
|
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')
|
||||||
|
|
|
@ -1,11 +1,33 @@
|
||||||
|
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 django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib import messages
|
||||||
from .models import Conversation
|
|
||||||
|
|
||||||
|
|
||||||
def conversation_details(request, conversation):
|
from accounts.models import Participation
|
||||||
conversation = get_object_or_404(Conversation, id=conversation, speaker__site=get_current_site(request))
|
from .models import Message
|
||||||
return render(request, 'conversations/message.html', {
|
from .forms import MessageForm
|
||||||
'conversation': conversation,
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def messaging(request):
|
||||||
|
|
||||||
|
participation = get_object_or_404(Participation, user=request.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():
|
||||||
|
message = form.save(commit=False)
|
||||||
|
message.conversation = conversation
|
||||||
|
message.author = request.user
|
||||||
|
message.save()
|
||||||
|
messages.success(request, 'Message sent!')
|
||||||
|
return redirect('messaging')
|
||||||
|
|
||||||
|
return render(request, 'conversations/messaging.html', {
|
||||||
|
'message_list': message_list,
|
||||||
|
'form': form,
|
||||||
})
|
})
|
||||||
|
|
|
@ -31,6 +31,17 @@ ALLOWED_HOSTS = []
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
# our apps
|
||||||
|
'accounts',
|
||||||
|
'ponyconf',
|
||||||
|
'proposals',
|
||||||
|
'conversations',
|
||||||
|
|
||||||
|
# external apps
|
||||||
|
'djangobower',
|
||||||
|
'bootstrap3',
|
||||||
|
'registration',
|
||||||
|
|
||||||
# build-in apps
|
# build-in apps
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
|
@ -39,17 +50,6 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
|
|
||||||
# external apps
|
|
||||||
'djangobower',
|
|
||||||
'bootstrap3',
|
|
||||||
'registration',
|
|
||||||
|
|
||||||
# our apps
|
|
||||||
'accounts',
|
|
||||||
'ponyconf',
|
|
||||||
'proposals',
|
|
||||||
'conversations',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = [
|
MIDDLEWARE_CLASSES = [
|
||||||
|
@ -178,4 +178,3 @@ BOOTSTRAP3 = {
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = ['yeouia.backends.YummyEmailOrUsernameInsensitiveAuth']
|
AUTHENTICATION_BACKENDS = ['yeouia.backends.YummyEmailOrUsernameInsensitiveAuth']
|
||||||
LOGOUT_REDIRECT_URL = 'home'
|
LOGOUT_REDIRECT_URL = 'home'
|
||||||
REPLY_EMAIL = 'pipo@example.org'
|
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-cog"></span> Administration</a></li>
|
<li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-cog"></span> Administration</a></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 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> {{ 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 %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.9.7 on 2016-06-12 21:41
|
# Generated by Django 1.9.7 on 2016-06-13 18:50
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import autoslug.fields
|
import autoslug.fields
|
||||||
|
@ -67,6 +67,6 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='speech',
|
name='speech',
|
||||||
unique_together=set([('order', 'talk'), ('speaker', 'talk')]),
|
unique_together=set([('speaker', 'talk'), ('order', 'talk')]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.contrib.auth.models import User
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .models import Speech, Talk, Topic
|
from .models import Talk, Topic, Speech
|
||||||
|
|
||||||
|
|
||||||
class ProposalsTests(TestCase):
|
class ProposalsTests(TestCase):
|
||||||
|
|
|
@ -7,7 +7,6 @@ from django.core.exceptions import PermissionDenied
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
|
||||||
from accounts.models import Speaker
|
|
||||||
from proposals.forms import TalkForm
|
from proposals.forms import TalkForm
|
||||||
from proposals.models import Speech, Talk, Topic
|
from proposals.models import Speech, Talk, Topic
|
||||||
|
|
||||||
|
@ -62,7 +61,6 @@ def talk_edit(request, talk=None):
|
||||||
talk.site = site
|
talk.site = site
|
||||||
talk.save()
|
talk.save()
|
||||||
form.save_m2m()
|
form.save_m2m()
|
||||||
Speaker.on_site.get_or_create(user=request.user, site=site)
|
|
||||||
Speech.objects.create(speaker=request.user, talk=talk)
|
Speech.objects.create(speaker=request.user, talk=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())
|
||||||
|
|
Loading…
Reference in New Issue