add custom attributes support to Django user model

- support added to profile, registration and admin form
- new field type can be added to authentic2/attribute_kinds.py
- only two types now: string and siret (french enterprise identifier)
This commit is contained in:
Benjamin Dauvergne 2014-03-24 12:35:46 +01:00
parent 7de5f17574
commit b9c0f7e213
8 changed files with 214 additions and 56 deletions

View File

@ -3,51 +3,69 @@ from copy import deepcopy
from django.contrib import admin
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.admin import GroupAdmin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.models import Group, User
from .nonce.models import Nonce
from . import forms
from . import forms, models, admin_forms
if settings.DEBUG:
class NonceModelAdmin(admin.ModelAdmin):
list_display = ("value", "context", "not_on_or_after")
admin.site.register(Nonce, NonceModelAdmin)
class AttributeValueAdmin(admin.ModelAdmin):
list_display = ('content_type', 'owner', 'attribute',
'content')
admin.site.register(models.AttributeValue, AttributeValueAdmin)
admin.site.register(models.FederatedId)
admin.site.register(models.LogoutUrl)
admin.site.register(models.AuthenticationEvent)
admin.site.register(models.UserExternalId)
if settings.AUTH_USER_MODEL == 'authentic2.User':
import models
import admin_forms
from django.contrib.auth.admin import UserAdmin
class AuthenticUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name',
'email', 'nickname', 'url', 'company', 'phone',
'postal_address')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
class AuthenticUserAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name',)}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups')}),
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
)
form = admin_forms.UserChangeForm
add_form = admin_forms.UserCreationForm
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')}
),
)
form = admin_forms.UserChangeForm
add_form = admin_forms.UserCreationForm
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')}
),
)
def get_fieldsets(self, request, obj=None):
fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj))
if obj:
if not request.user.is_superuser:
fieldsets[2][1]['fields'] = filter(lambda x: x !=
'is_superuser', fieldsets[2][1]['fields'])
return fieldsets
def get_fieldsets(self, request, obj=None):
fieldsets = deepcopy(super(AuthenticUserAdmin, self).get_fieldsets(request, obj))
if obj:
if not request.user.is_superuser:
fieldsets[2][1]['fields'] = filter(lambda x: x !=
'is_superuser', fieldsets[2][1]['fields'])
qs = models.Attribute.objects.all()
insertion_idx = 2
else:
qs = models.Attribute.objects.filter(required=True)
insertion_idx = 1
if qs.exists():
fieldsets = list(fieldsets)
fieldsets.insert(insertion_idx,
(_('Attributes'), {'fields': [at.name for at in qs]}))
return fieldsets
admin.site.unregister(User)
admin.site.register(User, AuthenticUserAdmin)
class AttributeAdmin(admin.ModelAdmin):
list_display = ('label', 'kind', 'required',
'asked_on_registration', 'user_editable',
'user_visible')
admin.site.register(models.Attribute, AttributeAdmin)
admin.site.register(models.User, AuthenticUserAdmin)
class A2GroupAdmin(GroupAdmin):
form = forms.GroupAdminForm

View File

@ -1,22 +1,24 @@
from django import forms
from django.contrib.auth.forms import UserChangeForm as AuthUserChangeForm, UserCreationForm as AuthUserCreationForm
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import (UserChangeForm as
AuthUserChangeForm, UserCreationForm as
AuthUserCreationForm)
from authentic2.compat import get_user_model
from . import forms
class UserChangeForm(forms.UserAttributeFormMixin,
AuthUserChangeForm):
class UserChangeForm(AuthUserChangeForm):
class Meta(AuthUserChangeForm.Meta):
model = get_user_model()
class UserCreationForm(forms.UserAttributeFormMixin,
AuthUserCreationForm):
class UserCreationForm(AuthUserCreationForm):
class Meta(AuthUserCreationForm.Meta):
model = get_user_model()
def clean_username(self):
# Since User.username is unique, this check is redundant,
# but it sets a nicer error message than the ORM. See #13147.
@ -26,4 +28,4 @@ class UserCreationForm(AuthUserCreationForm):
User._default_manager.get(username=username)
except User.DoesNotExist:
return username
raise forms.ValidationError(self.error_messages['duplicate_username'])
raise ValidationError(self.error_messages['duplicate_username'])

View File

@ -43,7 +43,8 @@ class CustomIndexDashboard(Dashboard):
self.children.append(modules.ModelList(
_('Users and groups'),
models=('authentic2.models.User',
'django.contrib.auth.models.*'),
'django.contrib.auth.models.*',
'authentic2.models.Attribute'),
))
self.children.append(modules.ModelList(
_('SAML2'),

View File

@ -1,16 +1,55 @@
from django import forms
from django.contrib.auth import models as auth_models
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from authentic2.compat import get_user_model
from . import models
auth_models.User.USER_PROFILE = ('first_name', 'last_name', 'email')
User = get_user_model()
all_field_names = [field.name for field in User._meta.fields]
field_names = getattr(User, 'USER_PROFILE', all_field_names)
__USER_FORM_CLASS = None
class UserProfileForm(forms.ModelForm):
class UserAttributeFormMixin(object):
def __init__(self, *args, **kwargs):
super(UserAttributeFormMixin, self).__init__(*args, **kwargs)
self.attributes = self.get_attributes()
initial = {}
if 'instance' in kwargs:
content_type = ContentType.objects.get_for_model(self.instance)
for av in models.AttributeValue.objects.filter(
content_type=content_type,
object_id=self.instance.pk):
initial[av.attribute.name] = av.to_python()
for attribute in self.attributes:
iv = initial.get(attribute.name)
attribute.contribute_to_form(self, initial=iv)
def get_attributes(self):
return models.Attribute.objects.all()
def save_attributes(self):
for attribute in self.attributes:
attribute.set_value(self.instance,
self.cleaned_data[attribute.name])
def save(self, commit=True):
result = super(UserAttributeFormMixin, self).save(commit=commit)
if commit:
self.save_attributes()
else:
old = self.save_m2m
def save_m2m(*args, **kwargs):
old(*args, **kwargs)
self.save_attributes()
self.save_m2m = save_m2m
return result
class UserProfileForm(UserAttributeFormMixin, forms.ModelForm):
error_css_class = 'form-field-error'
required_css_class = 'form-field-required'
@ -20,6 +59,10 @@ class UserProfileForm(forms.ModelForm):
if field in self.fields:
self.fields[field].required = True
def get_attributes(self):
qs = super(UserProfileForm, self).get_attributes()
qs = qs.filter(user_visible=True, user_editable=True)
return qs
class Meta:
model = User

View File

@ -71,3 +71,8 @@ class FederatedIdManager(managers.PassThroughManager \
'id_format': id_format,
'id_value': id_value})
class AttributeValueQuerySet(QuerySet):
pass
AttributeValueManager = managers.PassThroughManager \
.for_queryset_class(AttributeValueQuerySet)

View File

@ -4,6 +4,8 @@ import urlparse
from django.core.exceptions import ImproperlyConfigured
from django.utils import timezone
from django.utils.http import urlquote
from django.conf import settings
from django.core import validators
from django.db import models
from django.core.mail import send_mail
@ -11,8 +13,12 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import (AbstractBaseUser, PermissionsMixin,
BaseUserManager, SiteProfileNotAvailable)
from django.contrib.auth import load_backend
from django.utils.http import urlquote
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from model_utils.managers import QueryManager
from . import attribute_kinds
try:
from django.contrib.contenttypes.fields import GenericForeignKey
@ -248,3 +254,82 @@ class FederatedId(models.Model):
id_value = models.TextField()
objects = managers.FederatedIdManager()
class Attribute(models.Model):
label = models.CharField(verbose_name=_('label'), max_length=63,
unique=True)
description = models.TextField(verbose_name=_('description'), blank=True)
name = models.SlugField(verbose_name=_('name'), max_length=256,
unique=True)
required = models.BooleanField(
verbose_name=_('required'),
blank=True)
asked_on_registration = models.BooleanField(
verbose_name=_('asked on registration'),
blank=True)
user_editable = models.BooleanField(
verbose_name=_('user editable'),
blank=True)
user_visible = models.BooleanField(
verbose_name=_('user visible'),
blank=True)
multiple = models.BooleanField(
verbose_name=_('multiple'),
blank=True)
kind = models.CharField(max_length=16,
verbose_name=_('kind'),
choices=attribute_kinds.get_choices())
objects = models.Manager()
registration_attributes = QueryManager(asked_on_registration=True)
user_attributes = QueryManager(user_editable=True)
def get_form_field(self, **kwargs):
kwargs['label'] = self.label
kwargs['required'] = self.required
if self.description:
kwargs['help_text'] = self.description
return attribute_kinds.get_form_field(self.kind, **kwargs)
def get_kind(self):
return attribute_kinds.get_kind(self.kind)
def contribute_to_form(self, form, **kwargs):
form.fields[self.name] = self.get_form_field(**kwargs)
def set_value(self, owner, value):
serialize = self.get_kind()['serialize']
content = serialize(value)
av, created = AttributeValue.objects.get_or_create(
content_type=ContentType.objects.get_for_model(owner),
object_id=owner.pk,
attribute=self,
defaults={'content': content})
if not created:
av.content = content
av.save()
def __unicode__(self):
return self.label
class Meta:
verbose_name = _('attribute definition')
verbose_name_plural = _('attribute definitions')
class AttributeValue(models.Model):
content_type = models.ForeignKey('contenttypes.ContentType')
object_id = models.PositiveIntegerField()
owner = GenericForeignKey('content_type', 'object_id')
attribute = models.ForeignKey('Attribute',
verbose_name=_('attribute'))
content = models.TextField(verbose_name=_('content'))
def to_python(self):
deserialize = self.attribute.get_kind()['deserialize']
return deserialize(self.content)
class Meta:
verbose_name = _('attribute value')
verbose_name_plural = _('attribute values')

View File

@ -1,18 +1,17 @@
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from django import forms
from django.forms import Form, CharField, PasswordInput
from .. import app_settings, compat
from .. import app_settings, compat, forms
class RegistrationForm(forms.Form):
class RegistrationForm(forms.UserAttributeFormMixin, Form):
error_css_class = 'form-field-error'
required_css_class = 'form-field-required'
password1 = forms.CharField(widget=forms.PasswordInput,
label=_("Password"))
password2 = forms.CharField(widget=forms.PasswordInput,
label=_("Password (again)"))
password1 = CharField(widget=PasswordInput, label=_("Password"))
password2 = CharField(widget=PasswordInput, label=_("Password (again)"))
def __init__(self, *args, **kwargs):
"""
@ -47,7 +46,7 @@ class RegistrationForm(forms.Form):
User = compat.get_user_model()
existing = User.objects.filter(username__iexact=self.cleaned_data['username'])
if existing.exists():
raise forms.ValidationError(_("A user with that username already exists."))
raise ValidationError(_("A user with that username already exists."))
else:
return self.cleaned_data['username']
@ -58,7 +57,7 @@ class RegistrationForm(forms.Form):
User = compat.get_user_model()
if app_settings.A2_REGISTRATION_EMAIL_IS_UNIQUE:
if User.objects.filter(email__iexact=self.cleaned_data['email']):
raise forms.ValidationError(_('This email address is already in '
raise ValidationError(_('This email address is already in '
'use. Please supply a different email address.'))
return self.cleaned_data['email']
@ -71,5 +70,5 @@ class RegistrationForm(forms.Form):
"""
if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data:
if self.cleaned_data['password1'] != self.cleaned_data['password2']:
raise forms.ValidationError(_("The two password fields didn't match."))
raise ValidationError(_("The two password fields didn't match."))
return self.cleaned_data

View File

@ -42,6 +42,11 @@ class RegistrationView(BaseRegistrationView):
new_user.clean()
new_user.set_password(cleaned_data['password1'])
new_user.save()
attributes = models.Attribute.objects.filter(
asked_on_registration=True)
if attributes:
for attribute in attributes:
attribute.set_value(new_user, cleaned_data[attribute.name])
registration_profile = RegistrationProfile.objects.create_profile(new_user)
registration_profile.send_activation_email(site)