Skip to content

Commit 6736b71

Browse files
committed
Use the user model as the email model.
You can customize the user model so that the email is the username, in cases like that it may be superflouous to have multiple emails, in fact it may not be desirable. For applications like that it's easier to only use the email address defined in the user model.
1 parent 0b67aec commit 6736b71

File tree

8 files changed

+97
-22
lines changed

8 files changed

+97
-22
lines changed

account/auth_backends.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88

99
class UsernameAuthenticationBackend(ModelBackend):
10+
"""
11+
When the email model is the User, this authentication works just fine
12+
for authenticating the user through email
13+
"""
1014

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

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

3339
if username is None or password is None:

account/conf.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import importlib
22

3+
from django.apps import apps as django_apps
34
from django.conf import settings # noqa
45
from django.core.exceptions import ImproperlyConfigured
56

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

2425

25-
class AccountAppConf(AppConf):
26+
def get_email_model():
27+
"""
28+
Return the Email model that is active in this project.
29+
This should either be the user model, or the email model provided
30+
in the project
31+
"""
32+
try:
33+
return django_apps.get_model(settings.ACCOUNT_EMAIL_MODEL, require_ready=False)
34+
except ValueError:
35+
raise ImproperlyConfigured("ACCOUNT_EMAIL_MODEL must be of the form 'app_label.model_name'")
36+
except LookupError:
37+
raise ImproperlyConfigured(
38+
"ACCOUNT_EMAIL_MODEL refers to model '%s' that has not been installed" % settings.ACCOUNT_EMAIL_MODEL
39+
)
40+
2641

42+
def user_as_email():
43+
return settings.ACCOUNT_EMAIL_MODEL == settings.AUTH_USER_MODEL
44+
45+
46+
class AccountAppConf(AppConf):
47+
# TODO can we make a check that it's easier the user model or this model?
48+
EMAIL_MODEL = "account.EmailAddress"
2749
OPEN_SIGNUP = True
2850
LOGIN_URL = "account_login"
2951
LOGOUT_URL = "account_logout"

account/forms.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
from django.utils.encoding import force_str
88
from django.utils.translation import gettext_lazy as _
99

10-
from account.conf import settings
10+
from account.conf import settings, get_email_model
1111
from account.hooks import hookset
1212
from account.models import EmailAddress
1313
from account.utils import get_user_lookup_kwargs
1414

15-
alnum_re = re.compile(r"^\w+$")
15+
alnum_re = re.compile(r"^[\w+.@]+$")
16+
17+
EmailModel = get_email_model()
1618

1719

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

3335

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

3639
username = forms.CharField(
3740
label=_("Username"),
@@ -179,7 +182,7 @@ class PasswordResetForm(forms.Form):
179182

180183
def clean_email(self):
181184
value = self.cleaned_data["email"]
182-
if not EmailAddress.objects.filter(email__iexact=value).exists():
185+
if not EmailModel.objects.filter(email__iexact=value).exists():
183186
raise forms.ValidationError(_("Email address can not be found."))
184187
return value
185188

@@ -221,7 +224,7 @@ def clean_email(self):
221224
value = self.cleaned_data["email"]
222225
if self.initial.get("email") == value:
223226
return value
224-
qs = EmailAddress.objects.filter(email__iexact=value)
227+
qs = EmailModel.objects.filter(email__iexact=value)
225228
if not qs.exists() or not settings.ACCOUNT_EMAIL_UNIQUE:
226229
return value
227230
raise forms.ValidationError(_("A user is registered with this email address."))

account/managers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from django.db import models
22

3+
from account.hooks import hookset
4+
35

46
class EmailAddressManager(models.Manager):
57

68
def add_email(self, user, email, **kwargs):
79
confirm = kwargs.pop("confirm", False)
810
email_address = self.create(user=user, email=email, **kwargs)
911
if confirm and not email_address.verified:
10-
email_address.send_confirmation()
12+
self.send_confirmation(email=email)
1113
return email_address
1214

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

26+
def send_confirmation(self, **kwargs):
27+
from account.models import EmailConfirmation
28+
confirmation = EmailConfirmation.create(kwargs['email'])
29+
confirmation.send(**kwargs)
30+
return confirmation
31+
2432

2533
class EmailConfirmationManager(models.Manager):
2634

2735
def delete_expired_confirmations(self):
2836
for confirmation in self.all():
2937
if confirmation.key_expired():
3038
confirmation.delete()
39+
40+
41+
class UserEmailManager(models.Manager):
42+
"""This manager needs to be inherited if you use the User
43+
model as the email model.
44+
"""
45+
46+
def send_confirmation(self, **kwargs):
47+
from account.models import EmailConfirmation
48+
confirmation = EmailConfirmation.create(self)
49+
confirmation.send(**kwargs)
50+
return confirmation

account/migrations/0001_initial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class Migration(migrations.Migration):
5959
('created', models.DateTimeField(default=django.utils.timezone.now)),
6060
('sent', models.DateTimeField(null=True)),
6161
('key', models.CharField(unique=True, max_length=64)),
62-
('email_address', models.ForeignKey(to='account.EmailAddress', on_delete=models.CASCADE)),
62+
('email_address', models.ForeignKey(to=settings.ACCOUNT_EMAIL_MODEL, on_delete=models.CASCADE)),
6363
],
6464
options={
6565
'verbose_name': 'email confirmation',

account/models.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import pytz
1717
from account import signals
18-
from account.conf import settings
18+
from account.conf import settings, user_as_email
1919
from account.fields import TimeZoneField
2020
from account.hooks import hookset
2121
from account.languages import DEFAULT_LANGUAGE
@@ -272,11 +272,6 @@ def set_as_primary(self, conditional=False):
272272
self.user.save()
273273
return True
274274

275-
def send_confirmation(self, **kwargs):
276-
confirmation = EmailConfirmation.create(self)
277-
confirmation.send(**kwargs)
278-
return confirmation
279-
280275
def change(self, new_email, confirm=True):
281276
"""
282277
Given a new email address, change self and re-confirm.
@@ -288,12 +283,12 @@ def change(self, new_email, confirm=True):
288283
self.verified = False
289284
self.save()
290285
if confirm:
291-
self.send_confirmation()
286+
EmailAddress.objects.send_confirmation(email=self)
292287

293288

294289
class EmailConfirmation(models.Model):
295290

296-
email_address = models.ForeignKey(EmailAddress, on_delete=models.CASCADE)
291+
email_address = models.ForeignKey(settings.ACCOUNT_EMAIL_MODEL, on_delete=models.CASCADE)
297292
created = models.DateTimeField(default=timezone.now)
298293
sent = models.DateTimeField(null=True)
299294
key = models.CharField(max_length=64, unique=True)
@@ -321,7 +316,8 @@ def confirm(self):
321316
if not self.key_expired() and not self.email_address.verified:
322317
email_address = self.email_address
323318
email_address.verified = True
324-
email_address.set_as_primary(conditional=True)
319+
if not user_as_email():
320+
email_address.set_as_primary(conditional=True)
325321
email_address.save()
326322
signals.email_confirmed.send(sender=self.__class__, email_address=email_address)
327323
return email_address
@@ -334,9 +330,11 @@ def send(self, **kwargs):
334330
current_site.domain,
335331
reverse(settings.ACCOUNT_EMAIL_CONFIRMATION_URL, args=[self.key])
336332
)
333+
334+
user = self.email_address.user if not user_as_email() else self
337335
ctx = {
338336
"email_address": self.email_address,
339-
"user": self.email_address.user,
337+
"user": user,
340338
"activate_url": activate_url,
341339
"current_site": current_site,
342340
"key": self.key,

account/views.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616
from django.views.generic.edit import FormView
1717

1818
from account import signals
19-
from account.conf import settings
19+
from account.conf import settings, user_as_email, get_email_model
2020
from account.forms import (
2121
ChangePasswordForm,
2222
LoginUsernameForm,
23+
LoginEmailForm,
2324
PasswordResetForm,
2425
PasswordResetTokenForm,
2526
SettingsForm,
@@ -37,6 +38,13 @@
3738
)
3839
from account.utils import default_redirect, get_form_data
3940

41+
EmailModel = get_email_model()
42+
43+
if user_as_email():
44+
LoginForm = LoginEmailForm
45+
else:
46+
LoginForm = LoginUsernameForm
47+
4048

4149
class PasswordMixin(object):
4250
"""
@@ -120,7 +128,7 @@ def create_password_history(self, form, user):
120128

121129

122130
class SignupView(PasswordMixin, FormView):
123-
131+
# TODO need to closely go through this.
124132
template_name = "account/signup.html"
125133
template_name_ajax = "account/ajax/signup.html"
126134
template_name_email_confirmation_sent = "account/email_confirmation_sent.html"
@@ -284,15 +292,20 @@ def create_email_address(self, form, **kwargs):
284292
kwargs.setdefault("primary", True)
285293
kwargs.setdefault("verified", False)
286294
if self.signup_code:
287-
kwargs["verified"] = self.created_user.email == self.signup_code.email if self.signup_code.email else False
295+
verified = self.created_user.email == self.signup_code.email if self.signup_code.email else False
296+
kwargs["verified"] = verified
297+
298+
if user_as_email():
299+
self.created_user = kwargs['verified']
300+
return self.created_user
288301
return EmailAddress.objects.add_email(self.created_user, self.created_user.email, **kwargs)
289302

290303
def use_signup_code(self, user):
291304
if self.signup_code:
292305
self.signup_code.use(user)
293306

294307
def send_email_confirmation(self, email_address):
295-
email_address.send_confirmation(site=get_current_site(self.request))
308+
EmailModel.objects.send_confirmation(email=email_address, site=get_current_site(self.request))
296309

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

355368
template_name = "account/login.html"
356369
template_name_ajax = "account/ajax/login.html"
357-
form_class = LoginUsernameForm
370+
form_class = LoginForm
358371
form_kwargs = {}
359372
redirect_field_name = "next"
360373

@@ -774,6 +787,10 @@ def update_email(self, form, confirm=None):
774787
confirm = settings.ACCOUNT_EMAIL_CONFIRMATION_EMAIL
775788
# @@@ handle multiple emails per user
776789
email = form.cleaned_data["email"].strip()
790+
if user_as_email():
791+
# When using the user as email we don't need to care about primary emails
792+
return
793+
777794
if not self.primary_email_address:
778795
user.email = email
779796
EmailAddress.objects.add_email(self.request.user, email, primary=True, confirm=confirm)

docs/usage.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,12 @@ are saved forever, allowing password history checking for new passwords.
293293
For an authenticated user, ``ExpiredPasswordMiddleware`` prevents retrieving or posting
294294
to any page except the password change page and log out page when the user password is expired.
295295
However, if the user is "staff" (can access the Django admin site), the password check is skipped.
296+
297+
298+
Using email from User model
299+
============================
300+
301+
TODO
302+
303+
The user model needs a field `verified`, it should also have a property `email`, which is used
304+
as an alias for the username which is the email.

0 commit comments

Comments
 (0)