conversation app (continuation)

This commit is contained in:
Élie Bouttier 2016-06-14 21:39:04 +02:00
parent e4f413adcc
commit d62a494d9f
25 changed files with 290 additions and 84 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

83
conversations/emails.py Normal file
View File

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

10
conversations/forms.py Normal file
View File

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

View File

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

View File

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

24
conversations/sieve-filter Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;Administration</a></li> <li><a href="{% url 'admin:index' %}"><span class="glyphicon glyphicon-cog"></span>&nbsp;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 %}

View File

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

View File

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

View File

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