-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
django-authuser, custom user model for everybody
- Loading branch information
Rocky Meza
committed
May 20, 2013
0 parents
commit f6b2190
Showing
8 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*.pyc | ||
*.egg-info/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
Copyright (c) 2013, Fusionbox, Inc. | ||
All rights reserved. | ||
|
||
Redistribution and use in source and binary forms, with or without | ||
modification, are permitted provided that the following conditions are met: | ||
|
||
- Redistributions of source code must retain the above copyright notice, this | ||
list of conditions and the following disclaimer. | ||
- Redistributions in binary form must reproduce the above copyright notice, | ||
this list of conditions and the following disclaimer in the documentation | ||
and/or other materials provided with the distribution. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
django-authuser | ||
--------------- | ||
|
||
A custom user model app for Django 1.5+ that features email as username and | ||
other things. It tries to stay true to the built-in user model for the most | ||
part. | ||
|
||
The main differences between authuser's User and django.contrib.auth's are | ||
|
||
- email as username | ||
- one name field instead of first_name, last_name (see | ||
`Falsehoods Programmers Believe About Names <http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/>`_) | ||
- A less intimidating ReadOnlyPasswordHashWidget. | ||
|
||
Installation | ||
============ | ||
|
||
Before you use this, you should probably read the documentation about `custom User models <https://docs.djangoproject.com/en/dev/topics/auth/customizing/#substituting-a-custom-user-model>`_. | ||
|
||
1. Install the package:: | ||
|
||
$ pip install -e git://github.com/fusionbox/django-authuser@master#egg=django-authuser-dev | ||
|
||
2. Add ``authuser`` to your ``INSTALLED_APPS``. | ||
|
||
3. Add the following to your settings.py:: | ||
|
||
AUTH_USER_MODEL = 'authuser.User' | ||
|
||
But it's supposed to be a *custom* user model! | ||
============================================== | ||
|
||
Making an auth model that only concerns itself with authentication and | ||
authorization just *might* be a good idea. I recommend you read these: | ||
|
||
- `The User-Profile Pattern in Django <http://www.fusionbox.com/blog/detail/the-user-profile-pattern-in-django/>`_ | ||
- `Williams, Master of the "Come From" <https://github.com/raganwald/homoiconic/blob/master/2011/11/COMEFROM.md>`_ | ||
|
||
However, there are many valid reasons for wanting a user model that you can | ||
change things on. Django-authuser allows you to that too. Django-authuser | ||
provides a couple of abstract classes for subclassing. | ||
|
||
:class:`authuser.models.AbstractEmailUser` | ||
A no-frills email as username model. | ||
|
||
:class:`authuser.models.AbstractNamedUser` | ||
Adds a name field. | ||
|
||
If want to make your custom User model, you can use one of these base classes. | ||
You don't need to follow steps 2 or 3 of `Installation`_. | ||
|
||
.. note:: | ||
|
||
If you are just adding some methods and properties to the User model, you | ||
should consider using a proxy model. | ||
|
||
Admin | ||
===== | ||
|
||
Django-authuser provides a couple of Admin classes. The default one is | ||
:class:`authuser.admin.NamedUserAdmin`, which provides an admin similar to | ||
:class:`django.contrib.auth`. If you are not using the | ||
:class:`AbstractNamedUser`, you might want the :class:`authuser.admin.UserAdmin` | ||
instead. In addition there is a :class:`StrippedUserAdmin` and a | ||
:class:`StrippedNamedUserAdmin` class that don't include the Important Dates | ||
section or the permission models if you want simpler versions of those. | ||
|
||
If you are using your own user model, authuser won't register an Admin class to | ||
avoid problems. If you define ``REQUIRED_FIELDS`` on your custom model, authuser | ||
will add those to the first fieldset. | ||
|
||
Forms | ||
===== | ||
|
||
Authuser provides the following Form classes: | ||
|
||
:class:`authuser.forms.UserCreationForm` | ||
Basically the same as django.contrib.auth, but respects ``USERNAME_FIELD`` | ||
and ``User.REQUIRED_FIELDS``. | ||
|
||
:class:`authuser.forms.UserChangeForm` | ||
A normal ModelForm that adds a ``ReadOnlyPasswordHashField`` with the | ||
``BetterReadOnlyPasswordHashWidget``. |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
from __future__ import unicode_literals | ||
|
||
import copy | ||
|
||
from django.contrib import admin | ||
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin | ||
from django.contrib.auth import get_user_model | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
from authuser.models import User | ||
from authuser.forms import UserCreationForm, AdminUserChangeForm | ||
|
||
USERNAME_FIELD = get_user_model().USERNAME_FIELD | ||
|
||
REQUIRED_FIELDS = (USERNAME_FIELD,) + tuple(get_user_model().REQUIRED_FIELDS) | ||
|
||
BASE_FIELDS = (None, { | ||
'fields': REQUIRED_FIELDS + ('password',), | ||
}) | ||
|
||
SIMPLE_PERMISSION_FIELDS = (_('Permissions'), { | ||
'fields': ('is_active', 'is_staff', 'is_superuser',), | ||
}) | ||
|
||
ADVANCED_PERMISSION_FIELDS = copy.deepcopy(SIMPLE_PERMISSION_FIELDS) | ||
ADVANCED_PERMISSION_FIELDS[1]['fields'] += ('groups', 'user_permissions',) | ||
|
||
DATE_FIELDS = (_('Important dates'), { | ||
'fields': ('last_login', 'date_joined',), | ||
}) | ||
|
||
|
||
class StrippedUserAdmin(DjangoUserAdmin): | ||
# The forms to add and change user instances | ||
add_form_template = None | ||
add_form = UserCreationForm | ||
form = AdminUserChangeForm | ||
|
||
# The fields to be used in displaying the User model. | ||
# These override the definitions on the base UserAdmin | ||
# that reference specific fields on auth.User. | ||
list_display = ('is_active', USERNAME_FIELD, 'is_superuser', 'is_staff',) | ||
list_display_links = (USERNAME_FIELD,) | ||
list_filter = ('is_superuser', 'is_staff', 'is_active',) | ||
fieldsets = ( | ||
BASE_FIELDS, | ||
SIMPLE_PERMISSION_FIELDS, | ||
) | ||
add_fieldsets = ( | ||
(None, { | ||
'fields': REQUIRED_FIELDS + ( | ||
'password1', | ||
'password2', | ||
), | ||
}), | ||
) | ||
search_fields = (USERNAME_FIELD,) | ||
ordering = None | ||
filter_horizontal = tuple() | ||
|
||
|
||
class StrippedNamedUserAdmin(StrippedUserAdmin): | ||
list_display = ('is_active', 'email', 'name', 'is_superuser', 'is_staff',) | ||
list_display_links = ('email', 'name',) | ||
search_fields = ('email', 'name',) | ||
|
||
|
||
class UserAdmin(StrippedNamedUserAdmin): | ||
fieldsets = ( | ||
BASE_FIELDS, | ||
ADVANCED_PERMISSION_FIELDS, | ||
DATE_FIELDS, | ||
) | ||
filter_horizontal = ('groups', 'user_permissions',) | ||
|
||
|
||
class NamedUserAdmin(UserAdmin, StrippedNamedUserAdmin): | ||
pass | ||
|
||
|
||
# If they are using authuser.User, register the admin. | ||
if get_user_model() == User: | ||
admin.register(User, NamedUserAdmin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
from __future__ import unicode_literals | ||
|
||
from django import forms | ||
from django.contrib.auth.forms import ReadOnlyPasswordHashField, ReadOnlyPasswordHashWidget | ||
from django.contrib.auth import get_user_model | ||
from django.utils.translation import ugettext_lazy as _ | ||
|
||
|
||
User = get_user_model() | ||
|
||
|
||
class BetterReadOnlyPasswordHashWidget(ReadOnlyPasswordHashWidget): | ||
""" | ||
A ReadOnlyPasswordHashWidget that has a less intimidating output. | ||
""" | ||
def render(self, name, value, attrs): | ||
from django.utils.safestring import mark_safe | ||
from django.forms.util import flatatt | ||
final_attrs = flatatt(self.build_attrs(attrs)) | ||
hidden = '<div{attrs}><strong>*************</strong></div>'.format( | ||
attrs=final_attrs | ||
) | ||
return mark_safe(hidden) | ||
|
||
|
||
class UserCreationForm(forms.ModelForm): | ||
""" | ||
A form for creating new users. Includes all the required | ||
fields, plus a repeated password. | ||
""" | ||
|
||
error_messages = { | ||
'password_mismatch': _("The two password fields didn't match."), | ||
} | ||
|
||
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) | ||
password2 = forms.CharField(label=_("Password confirmation"), | ||
widget=forms.PasswordInput, | ||
help_text=_("Enter the same password as above," | ||
" for verification.")) | ||
|
||
class Meta: | ||
model = User | ||
fields = (User.USERNAME_FIELD,) + tuple(User.REQUIRED_FIELDS) | ||
|
||
def clean_password2(self): | ||
# Check that the two password entries match | ||
password1 = self.cleaned_data.get("password1") | ||
password2 = self.cleaned_data.get("password2") | ||
if password1 and password2 and password1 != password2: | ||
raise forms.ValidationError(self.error_messages['password_mismatch']) | ||
return password2 | ||
|
||
def save(self, commit=True): | ||
# Save the provided password in hashed format | ||
user = super(UserCreationForm, self).save(commit=False) | ||
user.set_password(self.cleaned_data["password1"]) | ||
if commit: | ||
user.save() | ||
return user | ||
|
||
|
||
class UserChangeForm(forms.ModelForm): | ||
""" | ||
A form for updating users. Includes all the fields on | ||
the user, but replaces the password field with admin's | ||
password hash display field. | ||
""" | ||
password = ReadOnlyPasswordHashField(label=_("Password"), | ||
widget=BetterReadOnlyPasswordHashWidget) | ||
|
||
class Meta: | ||
model = User | ||
|
||
def clean_password(self): | ||
# Regardless of what the user provides, return the initial value. | ||
# This is done here, rather than on the field, because the | ||
# field does not have access to the initial value | ||
return self.initial["password"] | ||
|
||
|
||
class AdminUserChangeForm(UserChangeForm): | ||
def __init__(self, *args, **kwargs): | ||
super(AdminUserChangeForm, self).__init__(*args, **kwargs) | ||
if not self.fields['password'].help_text: | ||
self.fields['password'].help_text = _( | ||
"Raw passwords are not stored, so there is no way to see this" | ||
" user's password, but you can change the password using" | ||
" <a href=\"password/\">this form</a>.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
from __future__ import unicode_literals | ||
|
||
from django.db import models | ||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin | ||
from django.utils.translation import ugettext_lazy as _ | ||
from django.utils import timezone | ||
from django.utils.encoding import python_2_unicode_compatible | ||
from django.contrib.auth.models import BaseUserManager | ||
|
||
|
||
class UserManager(BaseUserManager): | ||
def create_user(self, email, password=None, **kwargs): | ||
email = UserManager.normalize_email(email) | ||
user = self.model(email=email, **kwargs) | ||
user.set_password(password) | ||
user.save(using=self._db) | ||
return user | ||
|
||
def create_superuser(self, **kwargs): | ||
user = self.create_user(**kwargs) | ||
user.is_superuser = True | ||
user.is_staff = True | ||
user.save(using=self._db) | ||
return user | ||
|
||
|
||
class AbstractEmailUser(AbstractBaseUser, PermissionsMixin): | ||
email = models.EmailField(_('email address'), max_length=255, unique=True, | ||
db_index=True,) | ||
|
||
is_staff = models.BooleanField(_('staff status'), default=False, | ||
help_text=_('Designates whether the user can log into this admin ' | ||
'site.')) | ||
is_active = models.BooleanField(_('active'), default=True, | ||
help_text=_('Designates whether this user should be treated as ' | ||
'active. Unselect this instead of deleting accounts.')) | ||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now) | ||
|
||
objects = UserManager() | ||
|
||
USERNAME_FIELD = 'email' | ||
REQUIRED_FIELDS = [] | ||
|
||
class Meta: | ||
abstract = True | ||
ordering = ['email'] | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
gavinwahl
Member
|
||
|
||
def get_full_name(self): | ||
return self.name | ||
|
||
def get_short_name(self): | ||
return self.name | ||
|
||
|
||
@python_2_unicode_compatible | ||
class AbstractNamedUser(AbstractEmailUser): | ||
name = models.CharField(_('name'), max_length=255) | ||
|
||
REQUIRED_FIELDS = ['name'] | ||
|
||
class Meta: | ||
abstract = True | ||
ordering = ['name', 'email'] | ||
|
||
def __str__(self): | ||
return '{name} <{email}>'.format( | ||
name=self.name, | ||
email=self.email, | ||
) | ||
|
||
|
||
class User(AbstractNamedUser): | ||
class Meta: | ||
swappable = 'AUTH_USER_MODEL' | ||
verbose_name = _('user') | ||
verbose_name_plural = _('users') |
Oops, something went wrong.
It appears that ordering is not inherited from Abstract base classes. Here's an excerpt from django code that would suggest abstract models don't pass on their ordering.