Skip to content

Commit

Permalink
chore: Refactor forms
Browse files Browse the repository at this point in the history
  • Loading branch information
rubengrill authored and relekang committed Sep 14, 2018
1 parent 81a184f commit eb33fb8
Show file tree
Hide file tree
Showing 27 changed files with 446 additions and 366 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[report]
show_missing = True
exclude_lines =
pragma: no cover
raise NotImplementedError
if __name__ == .__main__.:
if settings.DEBUG:
Expand Down
2 changes: 1 addition & 1 deletion nopassword/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

try:
from .sms import TwilioBackend # noqa
except ImportError:
except ImportError: # pragma: no cover
pass
2 changes: 1 addition & 1 deletion nopassword/backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ def authenticate(self, request, username=None, code=None, **kwargs):
except (get_user_model().DoesNotExist, LoginCode.DoesNotExist):
return

def send_login_code(self, code, secure=False, host=None, **kwargs):
def send_login_code(self, code, context, **kwargs):
raise NotImplementedError
30 changes: 17 additions & 13 deletions nopassword/backends/email.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
# -*- coding: utf-8 -*-
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _

from .base import NoPasswordBackend
from nopassword.backends.base import NoPasswordBackend


class EmailBackend(NoPasswordBackend):
template_name = 'registration/login_code_request_email.html'
html_template_name = None
subject_template_name = 'registration/login_code_request_subject.txt'
from_email = None

def send_login_code(self, code, secure=False, host=None, **kwargs):
subject = getattr(settings, 'NOPASSWORD_LOGIN_EMAIL_SUBJECT', _('Login code'))
to_email = [code.user.email]
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', '[email protected]')
def send_login_code(self, code, context, **kwargs):
to_email = code.user.email
subject = render_to_string(self.subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = render_to_string(self.template_name, context)

context = {'url': code.login_url(secure=secure, host=host), 'code': code}
text_content = render_to_string('registration/login_email.txt', context)
html_content = render_to_string('registration/login_email.html', context)
email_message = EmailMultiAlternatives(subject, body, self.from_email, [to_email])

msg = EmailMultiAlternatives(subject, text_content, from_email, to_email)
msg.attach_alternative(html_content, 'text/html')
msg.send()
if self.html_template_name is not None:
html_email = render_to_string(self.html_template_name, context)
email_message.attach_alternative(html_email, 'text/html')

email_message.send()
11 changes: 6 additions & 5 deletions nopassword/backends/sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@
from django.template.loader import render_to_string
from twilio.rest import TwilioRestClient

from .base import NoPasswordBackend
from nopassword.backends.base import NoPasswordBackend


class TwilioBackend(NoPasswordBackend):
template_name = 'registration/login_code_request_sms.txt'
from_number = None

def __init__(self):
self.twilio_client = TwilioRestClient(
settings.NOPASSWORD_TWILIO_SID,
settings.NOPASSWORD_TWILIO_AUTH_TOKEN
)
super(TwilioBackend, self).__init__()

def send_login_code(self, code, secure=False, host=None, **kwargs):
def send_login_code(self, code, context, **kwargs):
"""
Send a login code via SMS
"""
from_number = getattr(settings, 'DEFAULT_FROM_NUMBER')

context = {'url': code.login_url(secure=secure, host=host), 'code': code}
from_number = self.from_number or getattr(settings, 'DEFAULT_FROM_NUMBER')
sms_content = render_to_string('registration/login_sms.txt', context)

self.twilio_client.messages.create(
Expand Down
146 changes: 106 additions & 40 deletions nopassword/forms.py
Original file line number Diff line number Diff line change
@@ -1,67 +1,133 @@
# -*- coding: utf-8 -*-
from django import forms
from django.contrib.auth import get_user_model
from django.utils.text import capfirst
from django.conf import settings
from django.contrib.auth import authenticate, get_backends, get_user_model
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import resolve_url
from django.utils.translation import ugettext_lazy as _

from nopassword.models import LoginCode
from nopassword import models


class AuthenticationForm(forms.Form):
"""
Base class for authenticating users. Extend this to get a form that accepts
username logins.
"""
username = forms.CharField()

class LoginCodeRequestForm(forms.Form):
error_messages = {
'invalid_login': _("Please enter a correct username. "
"Note that it is case-sensitive."),
'no_cookies': _("Your Web browser doesn't appear to have cookies "
"enabled. Cookies are required for logging in."),
'invalid_username': _(
"Please enter a correct %(username)s. "
"Note that it is case-sensitive."
),
'inactive': _("This account is inactive."),
}

def __init__(self, request=None, *args, **kwargs):
"""
If request is passed in, the form will validate that cookies are
enabled. Note that the request (a HttpRequest object) must have set a
cookie with the key TEST_COOKIE_NAME and value TEST_COOKIE_VALUE before
running this validation.
"""
super(AuthenticationForm, self).__init__(*args, **kwargs)
def __init__(self, *args, **kwargs):
super(LoginCodeRequestForm, self).__init__(*args, **kwargs)

self.request = request
self.login_code = None
self.username_field = get_user_model()._meta.get_field(get_user_model().USERNAME_FIELD)
self.fields['username'].max_length = self.username_field.max_length or 254

if self.fields['username'].label is None:
self.fields['username'].label = capfirst(self.username_field.verbose_name)
self.fields['username'] = self.username_field.formfield()

def clean_username(self):
username = self.cleaned_data['username']

try:
user = get_user_model()._default_manager.get_by_natural_key(username)
except get_user_model().DoesNotExist:
raise forms.ValidationError(self.error_messages['invalid_login'])
raise forms.ValidationError(
self.error_messages['invalid_username'],
code='invalid_username',
params={'username': self.username_field.verbose_name},
)

if not user.is_active:
raise forms.ValidationError(self.error_messages['invalid_login'])
raise forms.ValidationError(
self.error_messages['inactive'],
code='inactive',
)

self.login_code = LoginCode.create_code_for_user(user)
self.cleaned_data['login_code'] = models.LoginCode.create_code_for_user(user)

return username

def clean(self):
self.check_for_test_cookie()
def save(self, request, login_url=None, domain_override=None, extra_context=None):
login_code = self.cleaned_data['login_code']
login_code.next = request.GET.get('next')
login_code.save()

if not domain_override:
current_site = get_current_site(request)
site_name = current_site.name
domain = current_site.domain
else:
site_name = domain = domain_override

url = '{}://{}{}?code={}&next={}'.format(
'https' if request.is_secure() else 'http',
domain,
resolve_url(login_url or settings.LOGIN_URL),
login_code.code,
login_code.next,
)

context = {
'domain': domain,
'site_name': site_name,
'code': login_code.code,
'url': url,
}

if extra_context:
context.update(extra_context)

self.send_login_code(login_code, context)

def send_login_code(self, login_code, context, **kwargs):
for backend in get_backends():
if hasattr(backend, 'send_login_code'):
backend.send_login_code(login_code, context, **kwargs)
break
else:
raise ImproperlyConfigured(
'Please add a nopassword authentication backend to settings, '
'e.g. `nopassword.backends.EmailBackend`'
)


class LoginForm(forms.Form):
code = forms.ModelChoiceField(
label=_('Login code'),
queryset=models.LoginCode.objects.select_related('user'),
to_field_name='code',
widget=forms.TextInput,
error_messages={
'invalid_choice': _('Login code is invalid. It might have expired.'),
},
)

error_messages = {
'invalid_code': _("Unable to log in with provided login code."),
}

def __init__(self, request=None, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)

self.request = request

def clean_code(self):
code = self.cleaned_data['code']
username = code.user.get_username()
user = authenticate(self.request, **{
get_user_model().USERNAME_FIELD: username,
'code': code.code,
})

if not user:
raise forms.ValidationError(
self.error_messages['invalid_code'],
code='invalid_code',
)

self.cleaned_data['user'] = user

def check_for_test_cookie(self):
if self.request and not self.request.session.test_cookie_worked():
raise forms.ValidationError(self.error_messages['no_cookies'])
return code

def save(self, request):
self.login_code.next = request.GET.get('next')
self.login_code.save()
self.login_code.send_login_code(secure=request.is_secure(), host=request.get_host())
def get_user(self):
return self.cleaned_data.get('user')
43 changes: 2 additions & 41 deletions nopassword/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
# -*- coding: utf-8 -*-
import hashlib
import os
from datetime import datetime

from django.conf import settings
from django.contrib.auth import get_backends
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

Expand All @@ -23,47 +19,12 @@ def __str__(self):
return "%s - %s" % (self.user, self.timestamp)

def save(self, *args, **kwargs):
if settings.USE_TZ:
self.timestamp = timezone.now()
else:
self.timestamp = datetime.now()
self.timestamp = timezone.now()

if not self.next:
self.next = '/'
super(LoginCode, self).save(*args, **kwargs)

def login_url(self, secure=False, host=None):
url_namespace = getattr(settings, 'NOPASSWORD_NAMESPACE', 'nopassword')
username = self.user.get_username()
host = host or getattr(settings, 'SERVER_URL', None) or 'example.com'
if getattr(settings, 'NOPASSWORD_HIDE_USERNAME', False):
view = reverse_lazy(
'{0}:login_with_code'.format(url_namespace),
args=[self.code]
),
else:
view = reverse_lazy(
'{0}:login_with_code_and_username'.format(url_namespace),
args=[username, self.code]
),

return '%s://%s%s?next=%s' % (
'https' if secure else 'http',
host,
view[0],
self.next
)

def send_login_code(self, secure=False, host=None, **kwargs):
for backend in get_backends():
if hasattr(backend, 'send_login_code'):
backend.send_login_code(self, secure=secure, host=host, **kwargs)
break
else:
raise ImproperlyConfigured(
'Please add a nopassword authentication backend to settings, '
'e.g. `nopassword.backends.EmailBackend`'
)
super(LoginCode, self).save(*args, **kwargs)

@classmethod
def create_code_for_user(cls, user, next=None):
Expand Down
Loading

0 comments on commit eb33fb8

Please sign in to comment.