Fix stuffs.

Maybe breaks other things, don't know, can't test mails for now.
This commit is contained in:
Guilhem Saurel 2016-06-20 01:00:08 +02:00
parent f4b7eb629d
commit b4490c8eee
27 changed files with 202 additions and 120 deletions

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from accounts.models import Profile, Participation
from accounts.models import Participation, Profile
admin.site.register(Profile) # FIXME extend user admin
admin.site.register(Participation)

View File

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

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-19 20:26
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_auto_20160615_2031'),
]
operations = [
migrations.RemoveField(
model_name='participation',
name='review_topics',
),
]

View File

@ -6,9 +6,9 @@ from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.db import models
from .utils import enum_to_choices, generate_user_uid
from proposals.models import Topic
from ponyconf.utils import enum_to_choices
from .utils import generate_user_uid
__all__ = ['Profile']
@ -41,9 +41,6 @@ class Participation(models.Model):
connector = models.IntegerField(choices=enum_to_choices(CONNECTORS), blank=True, null=True)
constraints = models.TextField(blank=True)
# Participe as reviewer for theses topics
review_topics = models.ManyToManyField(Topic, blank=True)
objects = models.Manager()
on_site = CurrentSiteManager()

View File

@ -1,14 +1,14 @@
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
from django.contrib import messages
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sites.shortcuts import get_current_site
from django.dispatch import receiver
from .models import Participation
@receiver(user_logged_in)
def on_user_logged_in(sender, request, **kwargs):
Participation.on_site.get_or_create(user=request.user, site=get_current_site(request))
def on_user_logged_in(sender, request, user, **kwargs):
Participation.on_site.get_or_create(user=user, site=get_current_site(request))
messages.success(request, 'Welcome!', fail_silently=True) # FIXME

View File

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block admintab %}active{% endblock %}
{% block content %}
<div class="page-header">
<h1>Participant: {{ participant }}</h1>
</div>
<dl class="dl-horizontal">
<dt>User</dt><dd>{{ participation.user.profil }}</dd>
<dt>Arrival</dt><dd>{{ participation.arrival }}</dd>
</dl>
<!-- TODO -->
{% endblock %}

View File

@ -3,7 +3,9 @@ from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import TestCase
from .models import Profile, Participation
from ponyconf.utils import full_link
from .models import Participation, Profile
ROOT_URL = 'accounts'
@ -19,7 +21,7 @@ class AccountTests(TestCase):
self.client.login(username='b', password='b')
for model in [Profile, Participation]:
item = model.objects.first()
self.assertEqual(self.client.get(item.get_absolute_url()).status_code, 200)
self.assertEqual(self.client.get(full_link(item)).status_code, 200)
self.assertTrue(str(item))
def test_views(self):
@ -40,3 +42,12 @@ class AccountTests(TestCase):
self.assertEqual(user.email, 'b@newdomain.com')
self.assertEqual(user.profile.biography, 'tester')
self.client.logout()
def test_participant_views(self):
self.assertEqual(self.client.get(reverse('participants')).status_code, 302)
self.client.login(username='b', password='b')
self.assertEqual(self.client.get(reverse('participants')).status_code, 403)
b = User.objects.get(username='b')
b.is_superuser = True
b.save()
self.assertEqual(self.client.get(reverse('participants')).status_code, 200)

View File

@ -8,5 +8,6 @@ urlpatterns = [
url(r'^profile$', views.profile, name='profile'),
url(r'^logout/$', auth_views.logout, {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'),
url(r'^admin/participants/$', views.participants, name='participants'),
url(r'^admin/participant/(?P<username>[\w.@+-]+)$', views.participant, name='show-participation'),
url(r'', include('django.contrib.auth.urls')),
]

View File

@ -1,8 +1,5 @@
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,9 +1,7 @@
from django.contrib import messages
from django.contrib.auth.decorators import login_required
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 django.shortcuts import get_object_or_404, render
from .forms import ProfileForm, UserForm
from .models import Participation
@ -35,3 +33,8 @@ def participants(request):
participation_list = Participation.on_site.all()
return render(request, 'admin/participants.html', {'participation_list': participation_list})
def participant(request, username):
return render(request, 'admin/participant.html',
{'participant': get_object_or_404(Participation, user__username=username)})

View File

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

View File

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

View File

@ -1,20 +1,18 @@
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
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .models import Message
from .utils import hexdigest_sha256
@csrf_exempt
@require_http_methods(["POST"])
@ -22,14 +20,14 @@ def email_recv(request):
if not hasattr(settings, 'REPLY_EMAIL') \
or not hasattr(settings, 'REPLY_KEY'):
return HttpResponse(status=501) # Not Implemented
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
raise HttpResponse(status=400) # Bad Request
msg = request.FILES['email']
if python_version < (3,):
@ -37,10 +35,6 @@ def email_recv(request):
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:
@ -63,11 +57,11 @@ def email_recv(request):
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(',')):
for _mto in map(lambda x: x.strip(), msg.get('To').split(',')):
m = p.match(_mto)
if m:
break
if not m: # no one matches
if not m: # no one matches
raise Http404
author = get_object_or_404(User, profile__email_token=m.group('dest'))

View File

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

View File

@ -1,13 +1,14 @@
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db import models
from .utils import generate_message_token, notify_by_email
from accounts.models import Participation
from proposals.models import Talk
from .utils import generate_message_token, notify_by_email
class Message(models.Model):
@ -27,6 +28,9 @@ class Message(models.Model):
def __str__(self):
return "Message from %s" % self.author
def get_absolute_url(self):
return self.conversation.get_absolute_url()
class Conversation(models.Model):
@ -36,7 +40,7 @@ class Conversation(models.Model):
abstract = True
class ConversationWithParticipant(Conversation):
class ConversationWithParticipant(Conversation):
participation = models.OneToOneField(Participation, related_name='conversation')
messages = GenericRelation(Message)
@ -44,12 +48,15 @@ class ConversationWithParticipant(Conversation):
uri = 'inbox'
template = 'participant_message'
def get_site(self):
return self.participation.site
def __str__(self):
return "Conversation with %s" % self.participation.user
def get_absolute_url(self):
return reverse('conversation', kwargs={'username': self.participation.user.username})
def get_site(self):
return self.participation.site
def new_message(self, message):
site = self.get_site()
subject = '[%s] Conversation with %s' % (site.name, self.participation.user.profile)
@ -84,12 +91,15 @@ class ConversationAboutTalk(Conversation):
uri = 'inbox'
template = 'talk_message'
def get_site(self):
return self.talk.site
def __str__(self):
return "Conversation about %s" % self.talk.title
def get_absolute_url(self):
self.talk.get_absolute_url()
def get_site(self):
return self.talk.site
def new_message(self, message):
site = self.get_site()
first = self.messages.first()

View File

@ -1,15 +1,12 @@
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver
from django.conf import settings
from django.core.urlresolvers import reverse
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import ConversationWithParticipant, ConversationAboutTalk, Message
from .utils import notify_by_email
from proposals.models import Talk, Topic
from proposals.signals import new_talk
from accounts.models import Participation
from proposals.models import Talk
from proposals.signals import new_talk
from .models import ConversationAboutTalk, ConversationWithParticipant, Message
@receiver(post_save, sender=Participation, dispatch_uid="Create ConversationWithParticipant")
@ -31,15 +28,14 @@ def create_conversation_about_talk(sender, instance, created, **kwargs):
@receiver(new_talk, dispatch_uid="Notify new talk")
def notify_new_talk(sender, instance, **kwargs):
# Subscribe reviewer for these topics to conversations
topics = instance.topics.all()
reviewers = User.objects.filter(participation__review_topics=topics).all()
reviewers = User.objects.filter(participation__topic__talk=instance)
instance.conversation.subscribers.add(*reviewers)
for user in instance.speakers.all():
participation = Participation.on_site.get(user=user)
participation.conversation.subscribers.add(*reviewers)
# Notification of this new talk
message = Message(conversation=instance.conversation, author=instance.proposer,
content='The talk has been proposed.')
content='The talk has been proposed.')
message.save()

View File

@ -1,27 +1,29 @@
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import TestCase
from accounts.models import Participation
from .models import Conversation, Message
from .utils import get_reply_addr
from .models import ConversationWithParticipant, Message
class ConversationTests(TestCase):
def setUp(self):
a, b = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'ab')
participation = Participation.objects.create(user=a, site=Site.objects.first())
conversation = Conversation.objects.create(speaker=speaker)
Message.objects.create(token='pipo', conversation=conversation, author=a, content='allo')
a, b, c = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abc')
pa, _ = Participation.objects.get_or_create(user=a, site=Site.objects.first())
conversation, _ = ConversationWithParticipant.objects.get_or_create(participation=pa)
Message.objects.create(content='allo', conversation=conversation, author=b)
def test_models(self):
self.assertEqual(str(Conversation.objects.first()), 'Conversation with a')
self.assertEqual(str(Message.objects.first()), 'Message from a')
self.assertEqual(str(ConversationWithParticipant.objects.first()), 'Conversation with a')
self.assertEqual(str(Message.objects.first()), 'Message from b')
def test_views(self):
self.assertEqual(self.client.get(Conversation.objects.first().get_absolute_url()).status_code, 200)
def test_utils(self):
ret = ['pipo+ 11183704aabfddb3d694ff4f24c0daadfa2d8d2193336e345f92a6fd3ffb6a19e7@example.org']
self.assertEqual(get_reply_addr(1, User.objects.first()), ret)
url = ConversationWithParticipant.objects.first().get_absolute_url()
self.assertEqual(self.client.get(url).status_code, 302)
self.client.login(username='c', password='c')
self.assertEqual(self.client.get(url).status_code, 403)
self.assertEqual(self.client.get(reverse('correspondents')).status_code, 200)
self.assertEqual(self.client.get(reverse('inbox')).status_code, 200)
self.client.post(reverse('inbox'), {'content': 'coucou'})

View File

@ -1,7 +1,6 @@
from django.conf.urls import url
from conversations import views, emails
from conversations import emails, views
urlpatterns = [
url(r'^recv/$', emails.email_recv),

View File

@ -1,11 +1,11 @@
from django.conf import settings
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
from django.conf import settings
from django.core import mail
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.crypto import get_random_string
def hexdigest_sha256(*args):
@ -47,9 +47,7 @@ def notify_by_email(template, data, subject, sender, dests, message_id, ref=None
email=settings.DEFAULT_FROM_EMAIL)
# Generating headers
headers = {
'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL),
}
headers = {'Message-ID': "<%s.%s>" % (message_id, settings.DEFAULT_FROM_EMAIL)}
if ref:
# This email reference a previous one
headers.update({
@ -68,7 +66,8 @@ def notify_by_email(template, data, subject, sender, dests, message_id, ref=None
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 = 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:

View File

@ -1,15 +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 django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect, render
from accounts.models import Participation
from .models import Message
from .forms import MessageForm
@ -64,8 +62,7 @@ def subscribe(request, username):
# TODO check admin
participation = get_object_or_404(Participation, user__username=username,
site=get_current_site(request))
participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request))
participation.conversation.subscribers.add(request.user)
messages.success(request, 'Subscribed.')
@ -79,8 +76,7 @@ def unsubscribe(request, username):
# TODO check admin
participation = get_object_or_404(Participation, user__username=username,
site=get_current_site(request))
participation = get_object_or_404(Participation, user__username=username, site=get_current_site(request))
participation.conversation.subscribers.remove(request.user)
messages.success(request, 'Unsubscribed.')

View File

@ -179,4 +179,4 @@ BOOTSTRAP3 = {
AUTHENTICATION_BACKENDS = ['yeouia.backends.YummyEmailOrUsernameInsensitiveAuth']
LOGOUT_REDIRECT_URL = 'home'
ACCOUNT_ACTIVATION_DAYS = 7 # django-registration
ACCOUNT_ACTIVATION_DAYS = 7 # django-registration

10
ponyconf/utils.py Normal file
View File

@ -0,0 +1,10 @@
from django.contrib.sites.shortcuts import get_current_site
def enum_to_choices(enum):
return ((item.value, item.name) for item in list(enum))
def full_link(obj, request=None):
protocol = 'https' if request is None or request.is_secure() else 'http'
return '%s://%s%s' % (protocol, get_current_site(request), obj.get_absolute_url())

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-06-19 20:26
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_remove_participation_review_topics'),
('proposals', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='topic',
name='reviewers',
field=models.ManyToManyField(blank=True, to='accounts.Participation'),
),
]

View File

@ -8,8 +8,8 @@ from django.db import models
from autoslug import AutoSlugField
from accounts.utils import enum_to_choices
from accounts.models import Participation
from ponyconf.utils import enum_to_choices
__all__ = ['Topic', 'Talk', 'Speech']
@ -19,6 +19,8 @@ class Topic(models.Model):
name = models.CharField(max_length=128, verbose_name='Name', unique=True)
slug = AutoSlugField(populate_from='name', unique=True)
reviewers = models.ManyToManyField(Participation, blank=True)
def __str__(self):
return self.name
@ -54,13 +56,13 @@ class Talk(models.Model):
return True
if user == self.proposer:
return True
if user in talk.speakers.all():
if user in self.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()
return self.topics.filter(reviewers=participation).exists()
class Speech(models.Model):

View File

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

View File

@ -1,20 +1,25 @@
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.test import TestCase
from .models import Talk, Topic, Speech
from accounts.models import Participation
from .models import Speech, Talk, Topic
class ProposalsTests(TestCase):
def setUp(self):
for guy in 'ab':
User.objects.create_user(guy, email='%s@example.org' % guy, password=guy)
Topic.objects.create(name='pipo')
a, b, c = (User.objects.create_user(guy, email='%s@example.org' % guy, password=guy) for guy in 'abc')
Participation.objects.create(user=a, site=Site.objects.first())
Topic.objects.create(name='topipo')
c.is_superuser = True
c.save()
def test_everything(self):
# talk-edit
self.client.login(username='a', password='a')
self.client.post(reverse('add-talk'), {'title': 'super talk', 'description': 'super', 'event': 1})
self.client.post(reverse('add-talk'), {'title': 'super talk', 'description': 'super', 'event': 1, 'topics': 1})
self.assertEqual(str(Talk.on_site.first()), 'super talk')
self.client.post(reverse('edit-talk', kwargs={'talk': 'super-talk'}),
{'title': 'mega talk', 'description': 'mega', 'event': 1})
@ -41,3 +46,9 @@ class ProposalsTests(TestCase):
self.assertEqual(self.client.get(item.get_absolute_url()).status_code, 200)
self.assertTrue(str(item))
self.assertEqual(Speech.objects.first().username(), 'a')
# Talkis_editable_by
a, b, c = User.objects.all()
self.assertTrue(talk.is_editable_by(c))
Speech.objects.create(talk=talk, speaker=b, order=2)
self.assertTrue(talk.is_editable_by(b))

View File

@ -9,6 +9,7 @@ from django.views.generic import DetailView, ListView
from proposals.forms import TalkForm
from proposals.models import Speech, Talk, Topic
from .signals import new_talk
@ -48,7 +49,7 @@ def talk_edit(request, talk=None):
talk = get_object_or_404(Talk, slug=talk)
if talk.site != get_current_site(request):
raise PermissionDenied()
if not talk.has_perm(request.user):
if not talk.is_editable_by(request.user):
raise PermissionDenied()
form = TalkForm(request.POST or None, instance=talk)
if request.method == 'POST' and form.is_valid():
@ -72,10 +73,9 @@ def talk_edit(request, talk=None):
class TalkDetail(LoginRequiredMixin, DetailView):
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
return super().get_context_data(edit_perm=self.object.is_editable_by(self.request.user), **kwargs)
class TopicList(LoginRequiredMixin, ListView):