Skip to content

Commit

Permalink
django-authuser, custom user model for everybody
Browse files Browse the repository at this point in the history
  • Loading branch information
Rocky Meza committed May 20, 2013
0 parents commit f6b2190
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
*.egg-info/
22 changes: 22 additions & 0 deletions LICENSE
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.
83 changes: 83 additions & 0 deletions README.rst
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 added authuser/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions authuser/admin.py
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)
89 changes: 89 additions & 0 deletions authuser/forms.py
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>.")
76 changes: 76 additions & 0 deletions authuser/models.py
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.

Copy link
@rockymeza

rockymeza May 30, 2013

Contributor

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.

            if base_meta and not base_meta.abstract:
                # Non-abstract child classes inherit some attributes from their
                # non-abstract parent (unless an ABC comes before it in the
                # method resolution order).
                if not hasattr(meta, 'ordering'):
                    new_class._meta.ordering = base_meta.ordering
                if not hasattr(meta, 'get_latest_by'):
                    new_class._meta.get_latest_by = base_meta.get_latest_by

This comment has been minimized.

Copy link
@gavinwahl

gavinwahl May 30, 2013

Member

The docs state that if Meta inheritance is desired, you should explicitly inherit from your parent's Meta: https://docs.djangoproject.com/en/1.2/topics/db/models/#meta-inheritance

This comment has been minimized.

Copy link
@rockymeza

rockymeza May 30, 2013

Contributor

so should User.Meta inherit from AbstractNamedUser.Meta?

This comment has been minimized.

Copy link
@gavinwahl

gavinwahl May 30, 2013

Member

Fixed in e81d588


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')
Loading

0 comments on commit f6b2190

Please sign in to comment.