Skip to content

Commit

Permalink
Merge e5105da into b0bc4bd
Browse files Browse the repository at this point in the history
  • Loading branch information
bee-keeper committed Dec 1, 2015
2 parents b0bc4bd + e5105da commit 43bebfc
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 89 deletions.
117 changes: 117 additions & 0 deletions invitations/adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from django.template.loader import render_to_string
from django.contrib import messages
from django.conf import settings
from django.template import TemplateDoesNotExist
from django.core.mail import EmailMultiAlternatives, EmailMessage
from django.contrib.sites.models import Site

try:
from django.utils.encoding import force_text
except ImportError:
from django.utils.encoding import force_unicode as force_text

from .app_settings import app_settings


class BaseInvitationsAdapter(object):

def stash_verified_email(self, request, email):
request.session['account_verified_email'] = email

def unstash_verified_email(self, request):
ret = request.session.get('account_verified_email')
request.session['account_verified_email'] = None
return ret

def format_email_subject(self, subject):
site = Site.objects.get_current()
prefix = "[{name}] ".format(name=site.name)
return prefix + force_text(subject)

def render_mail(self, template_prefix, email, context):
"""
Renders an e-mail to `email`. `template_prefix` identifies the
e-mail that is to be sent, e.g. "account/email/email_confirmation"
"""
subject = render_to_string('{0}_subject.txt'.format(template_prefix),
context)
# remove superfluous line breaks
subject = " ".join(subject.splitlines()).strip()
subject = self.format_email_subject(subject)

bodies = {}
for ext in ['html', 'txt']:
try:
template_name = '{0}_message.{1}'.format(template_prefix, ext)
bodies[ext] = render_to_string(template_name,
context).strip()
except TemplateDoesNotExist:
if ext == 'txt' and not bodies:
# We need at least one body
raise
if 'txt' in bodies:
msg = EmailMultiAlternatives(subject,
bodies['txt'],
settings.DEFAULT_FROM_EMAIL,
[email])
if 'html' in bodies:
msg.attach_alternative(bodies['html'], 'text/html')
else:
msg = EmailMessage(subject,
bodies['html'],
settings.DEFAULT_FROM_EMAIL,
[email])
msg.content_subtype = 'html' # Main content is now text/html
return msg

def send_mail(self, template_prefix, email, context):
msg = self.render_mail(template_prefix, email, context)
msg.send()

def is_open_for_signup(self, request):
if hasattr(request, 'session') and request.session.get(
'account_verified_email'):
return True
elif app_settings.INVITATION_ONLY is True:
# Site is ONLY open for invites
return False
else:
# Site is open to signup
return True

def clean_email(self, email):
"""
Validates an email value. You can hook into this if you want to
(dynamically) restrict what email addresses can be chosen.
"""
return email

def add_message(self, request, level, message_template,
message_context=None, extra_tags=''):
"""
Wrapper of `django.contrib.messages.add_message`, that reads
the message text from a template.
"""
if 'django.contrib.messages' in settings.INSTALLED_APPS:
try:
if message_context is None:
message_context = {}
message = render_to_string(message_template,
message_context).strip()
if message:
messages.add_message(request, level, message,
extra_tags=extra_tags)
except TemplateDoesNotExist:
pass


def get_invitations_adapter():
if hasattr(settings, 'ACCOUNT_ADAPTER'):
if settings.ACCOUNT_ADAPTER == 'invitations.models.InvitationsAdapter':
# defer to allauth
from allauth.account.adapter import get_adapter
return get_adapter()
else:
from .utils import import_attribute
# load an adapter from elsewhere
return import_attribute(app_settings.ADAPTER)()
6 changes: 6 additions & 0 deletions invitations/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ def SIGNUP_REDIRECT(self):
""" Where to redirect on email confirm of invite """
return self._setting('SIGNUP_REDIRECT', 'account_signup')

@property
def ADAPTER(self):
""" The adapter, setting ACCOUNT_ADAPTER overrides this default """
return self._setting(
'ADAPTER', 'invitations.adapters.BaseInvitationsAdapter')

app_settings = AppSettings('INVITATIONS_')
5 changes: 2 additions & 3 deletions invitations/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model

from allauth.account.adapter import get_adapter

from .models import Invitation
from .adapters import get_invitations_adapter
from .exceptions import AlreadyInvited, AlreadyAccepted, UserRegisteredEmail


Expand All @@ -24,7 +23,7 @@ def validate_invitation(self, email):

def clean_email(self):
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
email = get_invitations_adapter().clean_email(email)

errors = {
"already_invited": _("This e-mail address has already been"
Expand Down
34 changes: 19 additions & 15 deletions invitations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@
from django.core.urlresolvers import reverse
from django.conf import settings

from allauth.account.adapter import DefaultAccountAdapter
from allauth.account.adapter import get_adapter

from .managers import InvitationManager
from .app_settings import app_settings
from .adapters import get_invitations_adapter
from . import signals


Expand Down Expand Up @@ -63,7 +61,7 @@ def send_invitation(self, request, **kwargs):

email_template = 'invitations/email/email_invite'

get_adapter().send_mail(
get_invitations_adapter().send_mail(
email_template,
self.email,
ctx)
Expand All @@ -80,15 +78,21 @@ def __str__(self):
return "Invite: {0}".format(self.email)


class InvitationsAdapter(DefaultAccountAdapter):
# here for backwards compatibility, historic allauth adapter
if hasattr(settings, 'ACCOUNT_ADAPTER'):

if settings.ACCOUNT_ADAPTER == 'invitations.models.InvitationsAdapter':
from allauth.account.adapter import DefaultAccountAdapter

class InvitationsAdapter(DefaultAccountAdapter):

def is_open_for_signup(self, request):
if hasattr(request, 'session') and request.session.get(
'account_verified_email'):
return True
elif app_settings.INVITATION_ONLY is True:
# Site is ONLY open for invites
return False
else:
# Site is open to signup
return True
def is_open_for_signup(self, request):
if hasattr(request, 'session') and request.session.get(
'account_verified_email'):
return True
elif app_settings.INVITATION_ONLY is True:
# Site is ONLY open for invites
return False
else:
# Site is open to signup
return True
117 changes: 79 additions & 38 deletions invitations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
from django.contrib.auth import get_user_model
from django.core import mail
from django.contrib.auth.models import AnonymousUser
from django.conf import settings

from allauth.account.adapter import get_adapter
from allauth.account.models import EmailAddress
from freezegun import freeze_time
from nose_parameterized import parameterized

from .models import Invitation, InvitationsAdapter
from .adapters import get_invitations_adapter, BaseInvitationsAdapter
from .models import Invitation
from .app_settings import app_settings
from .views import AcceptInvite, SendJSONInvite
from .forms import InviteForm
Expand Down Expand Up @@ -56,28 +56,31 @@ class InvitationsAdapterTests(TestCase):

@classmethod
def setUp(cls):
cls.adapter = get_adapter()
cls.signup_request = RequestFactory().get(reverse(
'account_signup', urlconf='allauth.account.urls'))
cls.adapter = get_invitations_adapter()

@classmethod
def tearDownClass(cls):
del cls.adapter

def test_fetch_adapter(self):
self.assertIsInstance(self.adapter, InvitationsAdapter)

def test_adapter_default_signup(self):
self.assertTrue(self.adapter.is_open_for_signup(self.signup_request))
if hasattr(settings, 'ACCOUNT_ADAPTER'):
from .models import InvitationsAdapter
self.assertIsInstance(self.adapter, InvitationsAdapter)
else:
self.assertIsInstance(self.adapter, BaseInvitationsAdapter)

@override_settings(
INVITATIONS_INVITATION_ONLY=True
INVITATIONS_INVITATION_ONLY=True,
)
def test_adapter_invitations_only(self):
self.assertFalse(self.adapter.is_open_for_signup(self.signup_request))
response = self.client.get(
reverse('account_signup'))
self.assertIn('Sign Up Closed', response.content.decode('utf8'))
def test_allauth_adapter_invitations_only(self):
if hasattr(settings, 'ACCOUNT_ADAPTER'):
signup_request = RequestFactory().get(reverse(
'account_signup', urlconf='allauth.account.urls'))
self.assertFalse(
self.adapter.is_open_for_signup(signup_request))
response = self.client.get(
reverse('account_signup'))
self.assertIn('Sign Up Closed', response.content.decode('utf8'))


class InvitationsSendViewTests(TestCase):
Expand All @@ -102,8 +105,14 @@ def test_auth(self):
response = self.client.post(
reverse('invitations:send-invite'), {'email': 'valid@example.com'},
follow=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, ['account/login.html'])

if hasattr(settings, 'ACCOUNT_ADAPTER'):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template_name, ['account/login.html'])

else:
self.assertEqual(response.status_code, 404)
self.assertEqual(response.request['PATH_INFO'], '/accounts/login/')

@parameterized.expand([
('invalid@example', 'Enter a valid email address'),
Expand Down Expand Up @@ -179,34 +188,58 @@ def test_accept_invite_invalid_key(self, method):
('get'),
('post'),
])
def test_accept_invite(self, method):
@override_settings(
INVITATIONS_SIGNUP_REDIRECT='/non-existent-url/'
)
def test_accept_invite_vanilla(self, method):
client_with_method = getattr(self.client, method)
resp = client_with_method(
reverse('invitations:accept-invite',
kwargs={'key': self.invitation.key}), follow=True)
invite = Invitation.objects.get(email='email@example.com')
self.assertTrue(invite.accepted)
self.assertEqual(invite.inviter, self.user)
self.assertEqual(resp.request['PATH_INFO'], reverse('account_signup'))

form = resp.context_data['form']
self.assertEqual('email@example.com', form.fields['email'].initial)
messages = resp.context['messages']
message_text = [message.message for message in messages]
self.assertEqual(
message_text, [
'Invitation to - email@example.com - has been accepted'])

resp = self.client.post(
reverse('account_signup'),
{'email': 'email@example.com',
'username': 'username',
'password1': 'password',
'password2': 'password'
})
resp.request['PATH_INFO'], '/non-existent-url/')

allauth_email_obj = EmailAddress.objects.get(email='email@example.com')
self.assertTrue(allauth_email_obj.verified)
@parameterized.expand([
('get'),
('post'),
])
def test_accept_invite_allauth(self, method):
if hasattr(settings, 'ACCOUNT_ADAPTER'):
# allauth specific
from allauth.account.models import EmailAddress

client_with_method = getattr(self.client, method)
resp = client_with_method(
reverse('invitations:accept-invite',
kwargs={'key': self.invitation.key}), follow=True)
invite = Invitation.objects.get(email='email@example.com')
self.assertTrue(invite.accepted)
self.assertEqual(invite.inviter, self.user)
self.assertEqual(
resp.request['PATH_INFO'], reverse('account_signup'))

form = resp.context_data['form']
self.assertEqual('email@example.com', form.fields['email'].initial)
messages = resp.context['messages']
message_text = [message.message for message in messages]
self.assertEqual(
message_text, [
'Invitation to - email@example.com - has been accepted'])

resp = self.client.post(
reverse('account_signup'),
{'email': 'email@example.com',
'username': 'username',
'password1': 'password',
'password2': 'password'
})

allauth_email_obj = EmailAddress.objects.get(
email='email@example.com')
self.assertTrue(allauth_email_obj.verified)

@override_settings(
INVITATIONS_SIGNUP_REDIRECT='/non-existent-url/'
Expand Down Expand Up @@ -245,6 +278,9 @@ def test_invite_url_sent_triggered_correctly(self, mock_signal):

invite.delete()

@override_settings(
INVITATIONS_SIGNUP_REDIRECT='/non-existent-url/'
)
@patch('invitations.signals.invite_accepted.send')
def test_invite_invite_accepted_triggered_correctly(self, mock_signal):
invite = Invitation.create('email@example.com')
Expand Down Expand Up @@ -339,7 +375,8 @@ class InvitationsJSONTests(TestCase):
def setUp(cls):
cls.user = get_user_model().objects.create_user(
username='flibble',
password='password')
password='password',
email='mrflibble@example.com')
cls.accepted_invite = Invitation.create('already@accepted.com')
cls.accepted_invite.accepted = True
cls.accepted_invite.save()
Expand All @@ -365,6 +402,10 @@ def tearDownClass(cls):
{u'valid': [],
u'invalid': [{u'email3@example.com': u'pending invite'}]},
400),
(['mrflibble@example.com'],
{u'valid': [],
u'invalid': [{u'mrflibble@example.com': u'user registered email'}]},
400),
(['example@example.com'],
{u'valid': [{u'example@example.com': u'invited'}],
u'invalid': []},
Expand Down
Loading

0 comments on commit 43bebfc

Please sign in to comment.