├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.md ├── emailusernames ├── __init__.py ├── admin.py ├── backends.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── createsuperuser.py │ │ ├── dumpdata.py │ │ └── loaddata.py ├── models.py ├── templates │ └── email_usernames │ │ └── login.html ├── tests.py └── utils.py ├── manage.py ├── setup.py └── testsettings.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | MANIFEST 3 | dist/ 4 | build/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | env: 8 | - DJANGO_VERSION=1.3 9 | - DJANGO_VERSION=1.4 10 | - DJANGO_VERSION=1.7 11 | - DJANGO_VERSION=1.8 12 | install: 13 | - pip install -q Django==$DJANGO_VERSION 14 | script: python manage.py test 15 | matrix: 16 | exclude: 17 | - python: "2.6" 18 | env: DJANGO_VERSION=1.8 19 | - python: "2.6" 20 | env: DJANGO_VERSION=1.7 21 | - python: "3.4" 22 | env: DJANGO_VERSION=1.3 23 | - python: "3.4" 24 | env: DJANGO_VERSION=1.4 25 | - python: "3.5" 26 | env: DJANGO_VERSION=1.3 27 | - python: "3.5" 28 | env: DJANGO_VERSION=1.4 29 | - python: "3.5" 30 | env: DJANGO_VERSION=1.7 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include emailusernames/templates/emailusernames * 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django Email as Username 2 | ======================== 3 | 4 | **User authentication with email addresses instead of usernames.** 5 | 6 | **Author:** Tom Christie, [@_tomchristie][twitter]. 7 | 8 | **See also:** [django-email-login], [django-email-usernames], [django-user-accounts]. 9 | 10 | [![Build Status][build-status-image]][travis] 11 | 12 | --- 13 | 14 | **Note**: As of Django 1.5 onwards you should consider using a [custom user model][docs] rather than using the `django-email-as-username` package. Original author is no longer accepting pull requests and issues against this package, but this fork is trying to keep it functional for old and new versions of Django. 15 | 16 | --- 17 | 18 | Overview 19 | ======== 20 | 21 | Allows you to treat users as having only email addresses, instead of usernames. 22 | 23 | 1. Provides an email auth backend and helper functions for creating users. 24 | 2. Patches the Django admin to handle email based user authentication. 25 | 3. Overides the `createsuperuser` command to create users with email only. 26 | 4. Treats email authentication as case-insensitive. 27 | 5. Correctly supports internationalised email addresses. 28 | 29 | 30 | Requirements 31 | ============ 32 | 33 | Known to work with Django >= 1.3 34 | 35 | 36 | Installation 37 | ============ 38 | 39 | Install from PyPI: 40 | 41 | pip install py2-py3-django-email-as-username 42 | 43 | Add `emailusernames` to `INSTALLED_APPS`. 44 | Make sure to include it further down the list than `django.contrib.auth`. 45 | 46 | INSTALLED_APPS = ( 47 | ... 48 | 'emailusernames', 49 | ) 50 | 51 | Set `EmailAuthBackend` as your authentication backend: 52 | 53 | AUTHENTICATION_BACKENDS = ( 54 | 'emailusernames.backends.EmailAuthBackend', 55 | # Uncomment the following to make Django tests pass: 56 | # 'django.contrib.auth.backends.ModelBackend', 57 | ) 58 | 59 | 60 | Usage 61 | ===== 62 | 63 | Creating users 64 | -------------- 65 | 66 | You should create users using the `create_user` and `create_superuser` 67 | functions. 68 | 69 | from emailusernames.utils import create_user, create_superuser 70 | 71 | create_user('me@example.com', 'password') 72 | create_superuser('admin@example.com', 'password') 73 | 74 | Retrieving users 75 | ---------------- 76 | 77 | You can retrieve users, using case-insensitive email matching, with the 78 | `get_user` function. Similarly you can use `user_exists` to test if a given 79 | user exists. 80 | 81 | from emailusernames.utils import get_user, user_exists 82 | 83 | user = get_user('someone@example.com') 84 | ... 85 | 86 | if user_exists('someone@example.com'): 87 | ... 88 | 89 | Both functions also take an optional queryset argument if you want to filter 90 | the set of users to retrieve. 91 | 92 | user = get_user('someone@example.com', 93 | queryset=User.objects.filter('profile__deleted=False')) 94 | 95 | Updating users 96 | -------------- 97 | 98 | You can update a user's email and save the instance, without having to also 99 | modify the username. 100 | 101 | user.email = 'other@example.com' 102 | user.save() 103 | 104 | Note that the `user.username` attribute will always return the email address, 105 | but behind the scenes it will be stored as a hashed version of the user's email. 106 | 107 | Authenticating users 108 | -------------------- 109 | 110 | You should use `email` and `password` keyword args in calls to `authenticate`, 111 | rather than the usual `username` and `password`. 112 | 113 | from django.contrib.auth import authenticate 114 | 115 | user = authenticate(email='someone@example.com', password='password') 116 | if user: 117 | ... 118 | else: 119 | ... 120 | 121 | User Forms 122 | ---------- 123 | 124 | `emailusernames` provides the following forms that you can use for 125 | authenticating, creating and updating users: 126 | 127 | * `emailusernames.forms.EmailAuthenticationForm` 128 | * `emailusernames.forms.EmailAdminAuthenticationForm` 129 | * `emailusernames.forms.EmailUserCreationForm` 130 | * `emailusernames.forms.EmailUserChangeForm` 131 | 132 | Using Django's built-in login view 133 | ---------------------------------- 134 | 135 | If you're using `django.contrib.auth.views.login` in your urlconf, you'll want 136 | to make sure you pass through `EmailAuthenticationForm` as an argument to 137 | the view. 138 | 139 | from emailusernames.forms import EmailAuthenticationForm 140 | 141 | urlpatterns = patterns('', 142 | ... 143 | url(r'^auth/login$', 'django.contrib.auth.views.login', 144 | {'authentication_form': EmailAuthenticationForm}, name='login'), 145 | ... 146 | ) 147 | 148 | 149 | Management commands 150 | =================== 151 | 152 | `emailusernames` will patch up the `syncdb` and `createsuperuser` managment 153 | commands, to ensure that they take email usernames. 154 | 155 | bash: ./manage.py syncdb 156 | ... 157 | You just installed Django's auth system, which means you don't have any superusers defined. 158 | Would you like to create one now? (yes/no): yes 159 | E-mail address: 160 | 161 | 162 | Migrating existing projects 163 | =========================== 164 | 165 | `emailusernames` includes a function you can use to easily migrate existing 166 | projects. 167 | 168 | The migration will refuse to run if there are any users that it cannot migrate 169 | either because they do not have an email set, or because there exists a 170 | duplicate email for more than one user. 171 | 172 | There are two ways you might choose to run this migration. 173 | 174 | Run the update manually 175 | ----------------------- 176 | 177 | Using `manage.py shell`: 178 | 179 | bash: python ./manage.py shell 180 | >>> from emailusernames.utils import migrate_usernames 181 | >>> migrate_usernames() 182 | Successfully migrated usernames for all 12 users 183 | 184 | Run as a data migration 185 | ----------------------- 186 | 187 | Using `south`, and assuming you have an app named `accounts`, this might look 188 | something like: 189 | 190 | bash: python ./manage.py datamigration accounts email_usernames 191 | Created 0002_email_usernames.py. 192 | 193 | Now edit `0002_email_usernames.py`: 194 | 195 | from emailusernames.utils import migrate_usernames 196 | 197 | def forwards(self, orm): 198 | "Write your forwards methods here." 199 | migrate_usernames() 200 | 201 | And finally apply the migration: 202 | 203 | python ./manage.py migrate accounts 204 | 205 | 206 | Running the tests 207 | ================= 208 | 209 | If you have cloned the source repo, you can run the tests using the 210 | provided `manage.py`: 211 | 212 | ./manage.py test 213 | 214 | Note that this application (unsurprisingly) breaks the existing 215 | `django.contrib.auth` tests. If your test suite currently includes those 216 | tests you'll need to find a way to explicitly disable them. 217 | 218 | Changelog 219 | ========= 220 | 221 | 1.7.1 222 | ----- 223 | * Fix check_for_test_cookie call for django > 1.6 224 | * Fix field order on EmailAuthenticationForm 225 | 226 | 227 | 1.7.0 228 | ----- 229 | 230 | * Fix compat with Django 1.7 & 1.8 231 | * Fix compat with python 3.5 232 | 233 | 1.6.7 234 | ----- 235 | 236 | * Fix compat with Django 1.6 237 | 238 | 1.6.6 239 | ----- 240 | 241 | * Allow users to be created with any explicitly specified primary key if required. 242 | 243 | 1.6.5 244 | ----- 245 | 246 | * Liberal `authenticate()` parameters, fixes some 3rd party integrations. 247 | * Fix templatetag compatibility with Django 1.5 248 | * Cleanup IntegrityError description for PostgreSQL 9.1 249 | 250 | 1.6.4 251 | ----- 252 | 253 | * Fix issue with migrating usernames. 254 | 255 | 1.6.3 256 | ----- 257 | 258 | * Fix issue when saving users via admin. 259 | 260 | 1.6.2 261 | ----- 262 | 263 | * Fix broken tests. 264 | * Added travis config. 265 | 266 | 1.6.1 267 | ----- 268 | 269 | * Fix screwed up packaging. 270 | 271 | 1.6.0 272 | ----- 273 | 274 | * Change field ordering in auth forms. 275 | * Fix handling of invalid emails in `createsuperuser` command. 276 | * `EmailAuthBackend` inherits from `ModelBackend`, fixing some permissions issues. 277 | * Fix `loaddata` and `savedata` fixture commands. 278 | 279 | 1.5.1 280 | ----- 281 | **To upgrade from <=1.4.6 you must also run the username migration 282 | as described above.** 283 | 284 | * Fix username hashing bug. 285 | 286 | 1.5.0 287 | ----- 288 | 289 | * Version bump, since the username hashes changed from 1.4.6 to 1.4.7. (Bumping to 1.5 should make it more obvious that users should check the changelog before upgrading.) 290 | 291 | 292 | 1.4.8 293 | ----- 294 | 295 | * Fix syntax error from 1.4.7 296 | 297 | 1.4.7 298 | ----- 299 | 300 | * Support for international domain names. 301 | * Fix auto-focus on login forms. 302 | 303 | 1.4.6 304 | ----- 305 | 306 | * EmailAuthenticationForm takes request as first argument, same as Django's 307 | AuthenticationForm. Now fixed so it won't break if you didn't specify 308 | data as a kwarg. 309 | 310 | 1.4.5 311 | ----- 312 | 313 | * Email form max lengths should be 75 chars, not 70 chars. 314 | * Use `get_static_prefix` (Supports 1.3 and 1.4.), not `admin_media_prefix`. 315 | 316 | 1.4.4 317 | ----- 318 | 319 | * Add 'queryset' argument to `get_user`, `user_exists` 320 | 321 | 1.4.3 322 | ----- 323 | 324 | * Fix support for loading users from fixtures. 325 | (Monkeypatch `User.save_base`, not `User.save`) 326 | 327 | 1.4.2 328 | ----- 329 | 330 | * Fix support for Django 1.4 331 | 332 | 1.4.1 333 | ----- 334 | 335 | * Fix bug with displaying usernames correctly if migration fails 336 | 337 | 1.4.0 338 | ----- 339 | 340 | * Easier migrations, using `migrate_usernames()` 341 | 342 | 1.3.1 343 | ----- 344 | 345 | * Authentication backend now sets `User.backend`. 346 | 347 | 1.3.0 348 | ----- 349 | 350 | * Use hashed username lookups for performance. 351 | * Use Django's email regex validator, rather than providing our own version. 352 | * Tweaks to admin. 353 | * Tweaks to documentation and notes on upgrading. 354 | 355 | 1.2.0 356 | ----- 357 | 358 | * Fix import bug in `createsuperuser` managment command. 359 | 360 | 1.1.0 361 | ----- 362 | 363 | * Fix bug in EmailAuthenticationForm 364 | 365 | 1.0.0 366 | ----- 367 | 368 | * Initial release 369 | 370 | License 371 | ======= 372 | 373 | Copyright © 2012-2013, DabApps. 374 | 375 | All rights reserved. 376 | 377 | Redistribution and use in source and binary forms, with or without 378 | modification, are permitted provided that the following conditions are met: 379 | 380 | Redistributions of source code must retain the above copyright notice, this 381 | list of conditions and the following disclaimer. 382 | Redistributions in binary form must reproduce the above copyright notice, this 383 | list of conditions and the following disclaimer in the documentation and/or 384 | other materials provided with the distribution. 385 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 386 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 387 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 388 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 389 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 390 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 391 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 392 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 393 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 394 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 395 | 396 | [twitter]: http://twitter.com/_tomchristie 397 | [django-email-login]: https://bitbucket.org/tino/django-email-login 398 | [django-email-usernames]: https://bitbucket.org/hakanw/django-email-usernames 399 | [django-user-accounts]: https://github.com/pinax/django-user-accounts/ 400 | [travis]: http://travis-ci.org/harmo/django-email-as-username?branch=master 401 | [build-status-image]: https://travis-ci.org/harmo/django-email-as-username.svg 402 | [docs]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#auth-custom-user 403 | -------------------------------------------------------------------------------- /emailusernames/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.7.1' 2 | -------------------------------------------------------------------------------- /emailusernames/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Override the add- and change-form in the admin, to hide the username. 3 | """ 4 | from django.contrib.auth.admin import UserAdmin 5 | from django.contrib.auth.models import User 6 | from django.contrib import admin 7 | from emailusernames.forms import EmailUserCreationForm, EmailUserChangeForm 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | 11 | class EmailUserAdmin(UserAdmin): 12 | add_form = EmailUserCreationForm 13 | form = EmailUserChangeForm 14 | 15 | add_fieldsets = ( 16 | (None, { 17 | 'classes': ('wide',), 18 | 'fields': ('email', 'password1', 'password2')} 19 | ), 20 | ) 21 | fieldsets = ( 22 | (None, {'fields': ('email', 'password')}), 23 | (_('Personal info'), {'fields': ('first_name', 'last_name')}), 24 | (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'user_permissions')}), 25 | (_('Important dates'), {'fields': ('last_login', 'date_joined')}), 26 | (_('Groups'), {'fields': ('groups',)}), 27 | ) 28 | list_display = ('email', 'first_name', 'last_name', 'is_staff') 29 | ordering = ('email',) 30 | 31 | 32 | admin.site.unregister(User) 33 | admin.site.register(User, EmailUserAdmin) 34 | 35 | 36 | def __email_unicode__(self): 37 | return self.email 38 | -------------------------------------------------------------------------------- /emailusernames/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.auth.backends import ModelBackend 3 | 4 | from emailusernames.utils import get_user 5 | 6 | 7 | class EmailAuthBackend(ModelBackend): 8 | 9 | """Allow users to log in with their email address""" 10 | 11 | def authenticate(self, request=None, email=None, password=None, **kwargs): 12 | # Some authenticators expect to authenticate by 'username' 13 | if email is None: 14 | email = kwargs.get('username') 15 | 16 | try: 17 | user = get_user(email) 18 | if user.check_password(password): 19 | user.backend = "%s.%s" % (self.__module__, self.__class__.__name__) 20 | return user 21 | except User.DoesNotExist: 22 | return None 23 | 24 | def get_user(self, user_id): 25 | try: 26 | return User.objects.get(pk=user_id) 27 | except User.DoesNotExist: 28 | return None 29 | -------------------------------------------------------------------------------- /emailusernames/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms, VERSION 2 | from django.contrib.auth import authenticate 3 | from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm 4 | from django.contrib.admin.forms import AdminAuthenticationForm 5 | from django.contrib.auth.models import User 6 | from django.utils.translation import ugettext_lazy as _ 7 | from emailusernames.utils import user_exists 8 | 9 | 10 | ERROR_MESSAGE = _("Please enter a correct email and password. ") 11 | ERROR_MESSAGE_RESTRICTED = _("You do not have permission to access the admin.") 12 | ERROR_MESSAGE_INACTIVE = _("This account is inactive.") 13 | 14 | 15 | class EmailAuthenticationForm(AuthenticationForm): 16 | """ 17 | Override the default AuthenticationForm to force email-as-username behavior. 18 | """ 19 | email = forms.EmailField(label=_("Email"), max_length=75) 20 | message_incorrect_password = ERROR_MESSAGE 21 | message_inactive = ERROR_MESSAGE_INACTIVE 22 | 23 | def __init__(self, request=None, *args, **kwargs): 24 | super(EmailAuthenticationForm, self).__init__(request, *args, **kwargs) 25 | if self.fields.get('username'): 26 | del self.fields['username'] 27 | if hasattr(self.fields, 'keyOrder'): 28 | # Django < 1.6 29 | self.fields.keyOrder = ['email', 'password'] 30 | else: 31 | # Django >= 1.6 32 | from collections import OrderedDict 33 | fields = OrderedDict() 34 | for key in ('email', 'password'): 35 | fields[key] = self.fields.pop(key) 36 | for key, value in self.fields.items(): 37 | fields[key] = value 38 | self.fields = fields 39 | 40 | def clean(self): 41 | email = self.cleaned_data.get('email') 42 | password = self.cleaned_data.get('password') 43 | 44 | if email and password: 45 | self.user_cache = authenticate(email=email, password=password) 46 | if (self.user_cache is None): 47 | raise forms.ValidationError(self.message_incorrect_password) 48 | if not self.user_cache.is_active: 49 | raise forms.ValidationError(self.message_inactive) 50 | # check_for_test_cookie was removed in django 1.7 51 | if hasattr(self, 'check_for_test_cookie'): 52 | self.check_for_test_cookie() 53 | return self.cleaned_data 54 | 55 | 56 | class EmailAdminAuthenticationForm(AdminAuthenticationForm): 57 | """ 58 | Override the default AuthenticationForm to force email-as-username behavior. 59 | """ 60 | email = forms.EmailField(label=_("Email"), max_length=75) 61 | message_incorrect_password = ERROR_MESSAGE 62 | message_inactive = ERROR_MESSAGE_INACTIVE 63 | message_restricted = ERROR_MESSAGE_RESTRICTED 64 | 65 | def __init__(self, *args, **kwargs): 66 | super(EmailAdminAuthenticationForm, self).__init__(*args, **kwargs) 67 | if self.fields.get('username'): 68 | del self.fields['username'] 69 | 70 | def clean(self): 71 | email = self.cleaned_data.get('email') 72 | password = self.cleaned_data.get('password') 73 | 74 | if email and password: 75 | self.user_cache = authenticate(email=email, password=password) 76 | if (self.user_cache is None): 77 | raise forms.ValidationError(self.message_incorrect_password) 78 | if not self.user_cache.is_active: 79 | raise forms.ValidationError(self.message_inactive) 80 | if not self.user_cache.is_staff: 81 | raise forms.ValidationError(self.message_restricted) 82 | # check_for_test_cookie was removed in django 1.7 83 | if hasattr(self, 'check_for_test_cookie'): 84 | self.check_for_test_cookie() 85 | return self.cleaned_data 86 | 87 | 88 | class EmailUserCreationForm(UserCreationForm): 89 | """ 90 | Override the default UserCreationForm to force email-as-username behavior. 91 | """ 92 | email = forms.EmailField(label=_("Email"), max_length=75) 93 | 94 | class Meta: 95 | model = User 96 | fields = ("email",) 97 | 98 | def __init__(self, *args, **kwargs): 99 | super(EmailUserCreationForm, self).__init__(*args, **kwargs) 100 | if self.fields.get('username'): 101 | del self.fields['username'] 102 | 103 | def clean_email(self): 104 | email = self.cleaned_data["email"] 105 | if user_exists(email): 106 | raise forms.ValidationError(_("A user with that email already exists.")) 107 | return email 108 | 109 | def save(self, commit=True): 110 | # Ensure that the username is set to the email address provided, 111 | # so the user_save_patch() will keep things in sync. 112 | self.instance.username = self.instance.email 113 | return super(EmailUserCreationForm, self).save(commit=commit) 114 | 115 | 116 | class EmailUserChangeForm(UserChangeForm): 117 | """ 118 | Override the default UserChangeForm to force email-as-username behavior. 119 | """ 120 | email = forms.EmailField(label=_("Email"), max_length=75) 121 | 122 | class Meta: 123 | model = User 124 | if VERSION[1] > 7: 125 | fields = '__all__' 126 | 127 | def __init__(self, *args, **kwargs): 128 | super(EmailUserChangeForm, self).__init__(*args, **kwargs) 129 | if self.fields.get('username'): 130 | del self.fields['username'] 131 | -------------------------------------------------------------------------------- /emailusernames/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harmo/django-email-as-username/549231832cd1bad0f3fb3d303e06d579ece94dfe/emailusernames/management/__init__.py -------------------------------------------------------------------------------- /emailusernames/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harmo/django-email-as-username/549231832cd1bad0f3fb3d303e06d579ece94dfe/emailusernames/management/commands/__init__.py -------------------------------------------------------------------------------- /emailusernames/management/commands/createsuperuser.py: -------------------------------------------------------------------------------- 1 | """ 2 | Management utility to create superusers. 3 | Replace default behaviour to use emails as usernames. 4 | """ 5 | 6 | import getpass 7 | import re 8 | import sys 9 | from optparse import make_option 10 | from django.contrib.auth.models import User 11 | from django.core import exceptions 12 | from django.core.management.base import BaseCommand, CommandError 13 | from django.utils.translation import ugettext as _ 14 | from emailusernames.utils import get_user, create_superuser 15 | 16 | def is_valid_email(value): 17 | # copied from https://github.com/django/django/blob/1.5.1/django/core/validators.py#L98 18 | email_re = re.compile( 19 | r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom 20 | # quoted-string, see also http://tools.ietf.org/html/rfc2822#section-3.2.5 21 | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' 22 | r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)$)' # domain 23 | r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) 24 | 25 | if not email_re.search(value): 26 | raise exceptions.ValidationError(_('Enter a valid e-mail address.')) 27 | 28 | 29 | class Command(BaseCommand): 30 | option_list = BaseCommand.option_list + ( 31 | make_option('--email', dest='email', default=None, 32 | help='Specifies the email address for the superuser.'), 33 | make_option('--noinput', action='store_false', dest='interactive', default=True, 34 | help=('Tells Django to NOT prompt the user for input of any kind. ' 35 | 'You must use --username and --email with --noinput, and ' 36 | 'superusers created with --noinput will not be able to log ' 37 | 'in until they\'re given a valid password.')), 38 | ) 39 | help = 'Used to create a superuser.' 40 | 41 | def handle(self, *args, **options): 42 | email = options.get('email', None) 43 | interactive = options.get('interactive') 44 | verbosity = int(options.get('verbosity', 1)) 45 | 46 | # Do quick and dirty validation if --noinput 47 | if not interactive: 48 | if not email: 49 | raise CommandError("You must use --email with --noinput.") 50 | try: 51 | is_valid_email(email) 52 | except exceptions.ValidationError: 53 | raise CommandError("Invalid email address.") 54 | 55 | # If not provided, create the user with an unusable password 56 | password = None 57 | 58 | # Prompt for username/email/password. Enclose this whole thing in a 59 | # try/except to trap for a keyboard interrupt and exit gracefully. 60 | if interactive: 61 | try: 62 | # Get an email 63 | while 1: 64 | if not email: 65 | email = raw_input('E-mail address: ') 66 | 67 | try: 68 | is_valid_email(email) 69 | except exceptions.ValidationError: 70 | sys.stderr.write("Error: That e-mail address is invalid.\n") 71 | email = None 72 | continue 73 | 74 | try: 75 | get_user(email) 76 | except User.DoesNotExist: 77 | break 78 | else: 79 | sys.stderr.write("Error: That email is already taken.\n") 80 | email = None 81 | 82 | # Get a password 83 | while 1: 84 | if not password: 85 | password = getpass.getpass() 86 | password2 = getpass.getpass('Password (again): ') 87 | if password != password2: 88 | sys.stderr.write("Error: Your passwords didn't match.\n") 89 | password = None 90 | continue 91 | if password.strip() == '': 92 | sys.stderr.write("Error: Blank passwords aren't allowed.\n") 93 | password = None 94 | continue 95 | break 96 | except KeyboardInterrupt: 97 | sys.stderr.write("\nOperation cancelled.\n") 98 | sys.exit(1) 99 | 100 | # Make Django's tests work by accepting a username through 101 | # call_command() but not through manage.py 102 | username = options.get('username', None) 103 | if username is None: 104 | create_superuser(email, password) 105 | else: 106 | User.objects.create_superuser(username, email, password) 107 | 108 | if verbosity >= 1: 109 | self.stdout.write("Superuser created successfully.\n") 110 | -------------------------------------------------------------------------------- /emailusernames/management/commands/dumpdata.py: -------------------------------------------------------------------------------- 1 | from django.core.management.commands import dumpdata 2 | from emailusernames.models import unmonkeypatch_user, monkeypatch_user 3 | 4 | 5 | class Command(dumpdata.Command): 6 | 7 | """ 8 | Override the built-in dumpdata command to un-monkeypatch the User 9 | model before dumping, to allow usernames to be dumped correctly 10 | """ 11 | 12 | def handle(self, *args, **kwargs): 13 | unmonkeypatch_user() 14 | ret = super(Command, self).handle(*args, **kwargs) 15 | monkeypatch_user() 16 | return ret 17 | -------------------------------------------------------------------------------- /emailusernames/management/commands/loaddata.py: -------------------------------------------------------------------------------- 1 | from django.core.management.commands import loaddata 2 | from emailusernames.models import unmonkeypatch_user, monkeypatch_user 3 | 4 | 5 | class Command(loaddata.Command): 6 | 7 | """ 8 | Override the built-in loaddata command to un-monkeypatch the User 9 | model before loading, to allow usernames to be loaded correctly 10 | """ 11 | 12 | def handle(self, *args, **kwargs): 13 | unmonkeypatch_user() 14 | ret = super(Command, self).handle(*args, **kwargs) 15 | monkeypatch_user() 16 | return ret 17 | 18 | -------------------------------------------------------------------------------- /emailusernames/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.sites import AdminSite 2 | from django.contrib.auth.models import User 3 | from emailusernames.forms import EmailAdminAuthenticationForm 4 | from emailusernames.utils import _email_to_username 5 | 6 | 7 | # Horrible monkey patching. 8 | # User.username always presents as the email, but saves as a hash of the email. 9 | # It would be possible to avoid such a deep level of monkey-patching, 10 | # but Django's admin displays the "Welcome, username" using user.username, 11 | # and there's really no other way to get around it. 12 | def user_init_patch(self, *args, **kwargs): 13 | super(User, self).__init__(*args, **kwargs) 14 | self._username = self.username 15 | if self.username == _email_to_username(self.email): 16 | # Username should be replaced by email, since the hashes match 17 | self.username = self.email 18 | 19 | 20 | def user_save_patch(self, *args, **kwargs): 21 | email_as_username = (self.username.lower() == self.email.lower()) 22 | if self.pk is not None: 23 | try: 24 | old_user = self.__class__.objects.get(pk=self.pk) 25 | email_as_username = ( 26 | email_as_username or 27 | ('@' in self.username and old_user.username == old_user.email) 28 | ) 29 | except self.__class__.DoesNotExist: 30 | pass 31 | 32 | if email_as_username: 33 | self.username = _email_to_username(self.email) 34 | try: 35 | super(User, self).save_base(*args, **kwargs) 36 | finally: 37 | if email_as_username: 38 | self.username = self.email 39 | 40 | 41 | original_init = User.__init__ 42 | original_save_base = User.save_base 43 | 44 | 45 | def monkeypatch_user(): 46 | User.__init__ = user_init_patch 47 | User.save_base = user_save_patch 48 | 49 | 50 | def unmonkeypatch_user(): 51 | User.__init__ = original_init 52 | User.save_base = original_save_base 53 | 54 | 55 | monkeypatch_user() 56 | 57 | 58 | # Monkey-path the admin site to use a custom login form 59 | AdminSite.login_form = EmailAdminAuthenticationForm 60 | AdminSite.login_template = 'email_usernames/login.html' 61 | -------------------------------------------------------------------------------- /emailusernames/templates/email_usernames/login.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/login.html" %} 2 | {% load i18n %} 3 | 4 | {% block extrastyle %}{{ block.super }} 5 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | {% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} 14 |
15 | {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 16 |
17 | {% endif %} 18 | 19 | {% if form.non_field_errors or form.this_is_the_login_form.errors %} 20 | {% for error in form.non_field_errors|add:form.this_is_the_login_form.errors %} 21 |22 | {{ error }} 23 |
24 | {% endfor %} 25 | {% endif %} 26 | 27 |