Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Use the user model as the email model. #353

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions account/auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@


class UsernameAuthenticationBackend(ModelBackend):
"""
When the email model is the User, this authentication works just fine
for authenticating the user through email
"""

def authenticate(self, request, username=None, password=None, **kwargs):
if username is None or password is None:
Expand All @@ -28,6 +32,8 @@ def authenticate(self, request, username=None, password=None, **kwargs):
class EmailAuthenticationBackend(ModelBackend):

def authenticate(self, request, username=None, password=None, **kwargs):
# TODO, remove primary true, username shouldn't be necessary either?
# should use email model. This probably doesn't need to be modified
qs = EmailAddress.objects.filter(Q(primary=True) | Q(verified=True))

if username is None or password is None:
Expand Down
24 changes: 23 additions & 1 deletion account/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib

from django.apps import apps as django_apps
from django.conf import settings # noqa
from django.core.exceptions import ImproperlyConfigured

Expand All @@ -22,8 +23,29 @@ def load_path_attr(path):
return attr


class AccountAppConf(AppConf):
def get_email_model():
"""
Return the Email model that is active in this project.
This should either be the user model, or the email model provided
in the project
"""
try:
return django_apps.get_model(settings.ACCOUNT_EMAIL_MODEL, require_ready=False)
except ValueError:
raise ImproperlyConfigured("ACCOUNT_EMAIL_MODEL must be of the form 'app_label.model_name'")
except LookupError:
raise ImproperlyConfigured(
"ACCOUNT_EMAIL_MODEL refers to model '%s' that has not been installed" % settings.ACCOUNT_EMAIL_MODEL
)


def user_as_email():
return settings.ACCOUNT_EMAIL_MODEL == settings.AUTH_USER_MODEL


class AccountAppConf(AppConf):
# TODO can we make a check that it's easier the user model or this model?
EMAIL_MODEL = "account.EmailAddress"
OPEN_SIGNUP = True
LOGIN_URL = "account_login"
LOGOUT_URL = "account_logout"
Expand Down
11 changes: 7 additions & 4 deletions account/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _

from account.conf import settings
from account.conf import settings, get_email_model
from account.hooks import hookset
from account.models import EmailAddress
from account.utils import get_user_lookup_kwargs

alnum_re = re.compile(r"^\w+$")
alnum_re = re.compile(r"^[\w+.@]+$")

EmailModel = get_email_model()


class PasswordField(forms.CharField):
Expand All @@ -32,6 +34,7 @@ def to_python(self, value):


class SignupForm(forms.Form):
# TODO make it possible to strip out the username field

username = forms.CharField(
label=_("Username"),
Expand Down Expand Up @@ -179,7 +182,7 @@ class PasswordResetForm(forms.Form):

def clean_email(self):
value = self.cleaned_data["email"]
if not EmailAddress.objects.filter(email__iexact=value).exists():
if not EmailModel.objects.filter(email__iexact=value).exists():
raise forms.ValidationError(_("Email address can not be found."))
return value

Expand Down Expand Up @@ -221,7 +224,7 @@ def clean_email(self):
value = self.cleaned_data["email"]
if self.initial.get("email") == value:
return value
qs = EmailAddress.objects.filter(email__iexact=value)
qs = EmailModel.objects.filter(email__iexact=value)
if not qs.exists() or not settings.ACCOUNT_EMAIL_UNIQUE:
return value
raise forms.ValidationError(_("A user is registered with this email address."))
22 changes: 21 additions & 1 deletion account/managers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from django.db import models

from account.hooks import hookset


class EmailAddressManager(models.Manager):

def add_email(self, user, email, **kwargs):
confirm = kwargs.pop("confirm", False)
email_address = self.create(user=user, email=email, **kwargs)
if confirm and not email_address.verified:
email_address.send_confirmation()
self.send_confirmation(email=email)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.send_confirmation(email=email)
self.send_confirmation(email=email_address)

return email_address

def get_primary(self, user):
Expand All @@ -21,10 +23,28 @@ def get_users_for(self, email):
# do a len() on it right away
return [address.user for address in self.filter(verified=True, email=email)]

def send_confirmation(self, **kwargs):
from account.models import EmailConfirmation
confirmation = EmailConfirmation.create(kwargs['email'])
confirmation.send(**kwargs)
return confirmation


class EmailConfirmationManager(models.Manager):

def delete_expired_confirmations(self):
for confirmation in self.all():
if confirmation.key_expired():
confirmation.delete()


class UserEmailManager(models.Manager):
"""This manager needs to be inherited if you use the User
model as the email model.
"""

def send_confirmation(self, **kwargs):
from account.models import EmailConfirmation
confirmation = EmailConfirmation.create(self)
confirmation.send(**kwargs)
return confirmation
2 changes: 1 addition & 1 deletion account/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Migration(migrations.Migration):
('created', models.DateTimeField(default=django.utils.timezone.now)),
('sent', models.DateTimeField(null=True)),
('key', models.CharField(unique=True, max_length=64)),
('email_address', models.ForeignKey(to='account.EmailAddress', on_delete=models.CASCADE)),
('email_address', models.ForeignKey(to=settings.ACCOUNT_EMAIL_MODEL, on_delete=models.CASCADE)),
],
options={
'verbose_name': 'email confirmation',
Expand Down
18 changes: 8 additions & 10 deletions account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import pytz
from account import signals
from account.conf import settings
from account.conf import settings, user_as_email
from account.fields import TimeZoneField
from account.hooks import hookset
from account.languages import DEFAULT_LANGUAGE
Expand Down Expand Up @@ -272,11 +272,6 @@ def set_as_primary(self, conditional=False):
self.user.save()
return True

def send_confirmation(self, **kwargs):
confirmation = EmailConfirmation.create(self)
confirmation.send(**kwargs)
return confirmation

def change(self, new_email, confirm=True):
"""
Given a new email address, change self and re-confirm.
Expand All @@ -288,12 +283,12 @@ def change(self, new_email, confirm=True):
self.verified = False
self.save()
if confirm:
self.send_confirmation()
EmailAddress.objects.send_confirmation(email=self)


class EmailConfirmation(models.Model):

email_address = models.ForeignKey(EmailAddress, on_delete=models.CASCADE)
email_address = models.ForeignKey(settings.ACCOUNT_EMAIL_MODEL, on_delete=models.CASCADE)
created = models.DateTimeField(default=timezone.now)
sent = models.DateTimeField(null=True)
key = models.CharField(max_length=64, unique=True)
Expand Down Expand Up @@ -321,7 +316,8 @@ def confirm(self):
if not self.key_expired() and not self.email_address.verified:
email_address = self.email_address
email_address.verified = True
email_address.set_as_primary(conditional=True)
if not user_as_email():
email_address.set_as_primary(conditional=True)
email_address.save()
signals.email_confirmed.send(sender=self.__class__, email_address=email_address)
return email_address
Expand All @@ -334,9 +330,11 @@ def send(self, **kwargs):
current_site.domain,
reverse(settings.ACCOUNT_EMAIL_CONFIRMATION_URL, args=[self.key])
)

user = self.email_address.user if not user_as_email() else self
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
user = self.email_address.user if not user_as_email() else self
user = self.email_address.user if not user_as_email() else self.email_address

ctx = {
"email_address": self.email_address,
"user": self.email_address.user,
"user": user,
"activate_url": activate_url,
"current_site": current_site,
"key": self.key,
Expand Down
27 changes: 22 additions & 5 deletions account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
from django.views.generic.edit import FormView

from account import signals
from account.conf import settings
from account.conf import settings, user_as_email, get_email_model
from account.forms import (
ChangePasswordForm,
LoginUsernameForm,
LoginEmailForm,
PasswordResetForm,
PasswordResetTokenForm,
SettingsForm,
Expand All @@ -37,6 +38,13 @@
)
from account.utils import default_redirect, get_form_data

EmailModel = get_email_model()

if user_as_email():
LoginForm = LoginEmailForm
else:
LoginForm = LoginUsernameForm


class PasswordMixin(object):
"""
Expand Down Expand Up @@ -120,7 +128,7 @@ def create_password_history(self, form, user):


class SignupView(PasswordMixin, FormView):

# TODO need to closely go through this.
template_name = "account/signup.html"
template_name_ajax = "account/ajax/signup.html"
template_name_email_confirmation_sent = "account/email_confirmation_sent.html"
Expand Down Expand Up @@ -284,15 +292,20 @@ def create_email_address(self, form, **kwargs):
kwargs.setdefault("primary", True)
kwargs.setdefault("verified", False)
if self.signup_code:
kwargs["verified"] = self.created_user.email == self.signup_code.email if self.signup_code.email else False
verified = self.created_user.email == self.signup_code.email if self.signup_code.email else False
kwargs["verified"] = verified

if user_as_email():
self.created_user = kwargs['verified']
return self.created_user
return EmailAddress.objects.add_email(self.created_user, self.created_user.email, **kwargs)

def use_signup_code(self, user):
if self.signup_code:
self.signup_code.use(user)

def send_email_confirmation(self, email_address):
email_address.send_confirmation(site=get_current_site(self.request))
EmailModel.objects.send_confirmation(email=email_address, site=get_current_site(self.request))

def after_signup(self, form):
signals.user_signed_up.send(sender=SignupForm, user=self.created_user, form=form)
Expand Down Expand Up @@ -354,7 +367,7 @@ class LoginView(FormView):

template_name = "account/login.html"
template_name_ajax = "account/ajax/login.html"
form_class = LoginUsernameForm
form_class = LoginForm
form_kwargs = {}
redirect_field_name = "next"

Expand Down Expand Up @@ -774,6 +787,10 @@ def update_email(self, form, confirm=None):
confirm = settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL
# @@@ handle multiple emails per user
email = form.cleaned_data["email"].strip()
if user_as_email():
# When using the user as email we don't need to care about primary emails
return

if not self.primary_email_address:
user.email = email
EmailAddress.objects.add_email(self.request.user, email, primary=True, confirm=confirm)
Expand Down
9 changes: 9 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,12 @@ are saved forever, allowing password history checking for new passwords.
For an authenticated user, ``ExpiredPasswordMiddleware`` prevents retrieving or posting
to any page except the password change page and log out page when the user password is expired.
However, if the user is "staff" (can access the Django admin site), the password check is skipped.


Using email from User model
============================

TODO

The user model needs a field `verified`, it should also have a property `email`, which is used
as an alias for the username which is the email.