diff --git a/README.rst b/README.rst index 18502e0..7e4e6ac 100644 --- a/README.rst +++ b/README.rst @@ -122,6 +122,19 @@ to your ``settings.py``: STORMPATH_SECRET = 'yourApiKeySecret' STORMPATH_APPLICATION = 'https://api.stormpath.com/v1/applications/YOUR_APP_UID_HERE' +If you have custom fields in you user model, you can update it while sign up or sign in +by defining STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA variable like this: + +.. code-block:: python + + STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA = { + 'facebook': "users.utils.retrieve_custom_data_facebook" + } + +Where `users.utils.retrieve_custom_data_facebook` is a function that return dict. +Key is the field name with django prefix, and value is the field. This function +Will be responsible for connecting to the provider and process the data. + Once this is done, you're ready to get started! The next thing you need to do is to sync your database and apply any migrations: diff --git a/django_stormpath/backends.py b/django_stormpath/backends.py index e3896a5..17ba53d 100644 --- a/django_stormpath/backends.py +++ b/django_stormpath/backends.py @@ -59,7 +59,7 @@ def _create_or_get_user(self, account): UserModel = get_user_model() try: user = UserModel.objects.get( - Q(username=account.username) | Q(email=account.email)) + **{UserModel.USERNAME_FIELD: getattr(account, UserModel.USERNAME_FIELD)}) user._mirror_data_from_stormpath_account(account) self._mirror_groups_from_stormpath() users_sp_groups = [g.name for g in account.groups] diff --git a/django_stormpath/forms.py b/django_stormpath/forms.py index df015a3..b4a0608 100644 --- a/django_stormpath/forms.py +++ b/django_stormpath/forms.py @@ -2,12 +2,11 @@ """ from django import forms -from django.contrib.auth import get_user_model from django.contrib.auth.forms import ReadOnlyPasswordHashField from stormpath.error import Error -from .models import APPLICATION +from .models import StormpathUser, APPLICATION class StormpathUserCreationForm(forms.ModelForm): @@ -22,7 +21,7 @@ class StormpathUserCreationForm(forms.ModelForm): widget=forms.PasswordInput) class Meta: - model = get_user_model() + model = StormpathUser fields = ("username", "email", "given_name", "surname", "password1", "password2") @@ -89,7 +88,7 @@ class StormpathUserChangeForm(forms.ModelForm): """Update Stormpath user form.""" class Meta: - model = get_user_model() + model = StormpathUser exclude = ('password',) password = ReadOnlyPasswordHashField(help_text=("Passwords are not stored in the local database " diff --git a/django_stormpath/models.py b/django_stormpath/models.py index 636f944..d6edb47 100644 --- a/django_stormpath/models.py +++ b/django_stormpath/models.py @@ -9,14 +9,17 @@ from django.conf import settings from django.db import models, IntegrityError, transaction -from django.contrib.auth.models import (BaseUserManager, - AbstractBaseUser, PermissionsMixin) +from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser, PermissionsMixin) from django.forms import model_to_dict from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import pre_save, pre_delete from django.contrib.auth.models import Group from django.dispatch import receiver from django import VERSION as django_version +from django.utils.dateparse import ( + parse_date, parse_datetime, parse_time +) from stormpath.client import Client from stormpath.error import Error as StormpathError @@ -143,37 +146,39 @@ def delete(self, *args, **kwargs): delete.queryset_only = True -class StormpathBaseUser(AbstractBaseUser, PermissionsMixin): - - class Meta: - abstract = True - +class StormpathMixin(models.Model): href = models.CharField(max_length=255, null=True, blank=True) - username = models.CharField(max_length=255, unique=True) given_name = models.CharField(max_length=255) surname = models.CharField(max_length=255) - middle_name = models.CharField(max_length=255, null=True, blank=True) - email = models.EmailField(verbose_name='email address', + email = models.EmailField( + verbose_name='email address', max_length=255, unique=True, db_index=True) - - STORMPATH_BASE_FIELDS = ['href', 'username', 'given_name', 'surname', 'middle_name', 'email', 'password'] - EXCLUDE_FIELDS = ['href', 'last_login', 'groups', 'id', 'stormpathpermissionsmixin_ptr', 'user_permissions'] - - PASSWORD_FIELD = 'password' + is_active = models.BooleanField(default=get_default_is_active) + is_verified = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['given_name', 'surname'] + STORMPATH_BASE_FIELDS = [ + 'href', 'username', 'given_name', 'surname', 'middle_name', 'email', + 'password'] + EXCLUDE_FIELDS = [ + 'href', 'last_login', 'groups', 'id', 'stormpathpermissionsmixin_ptr', + 'user_permissions'] + DATE_FIELDS = [] + TIME_FIELDS = [] + DATETIME_FIELDS = [] + FILE_FIELDS = [] + PASSWORD_FIELD = 'password' - is_active = models.BooleanField(default=get_default_is_active) - is_verified = models.BooleanField(default=False) - is_admin = models.BooleanField(default=False) - is_staff = models.BooleanField(default=False) + DJANGO_PREFIX = 'spDjango_' objects = StormpathUserManager() - DJANGO_PREFIX = 'spDjango_' + class Meta: + abstract = True @property def first_name(self): @@ -212,11 +217,19 @@ def _mirror_data_from_db_user(self, account, data): if 'is_active' in data: del data['is_active'] - for key in data: + for key, value in data.iteritems(): if key in self.STORMPATH_BASE_FIELDS: - account[key] = data[key] + account[key] = value else: - account.custom_data[self.DJANGO_PREFIX + key] = data[key] + # Matches datetime, date and time + if hasattr(value, 'isoformat'): + value = value.isoformat() + if key in self.FILE_FIELDS: + if value: + value = value.url + else: + value = None + account.custom_data[self.DJANGO_PREFIX + key] = value return account @@ -228,7 +241,17 @@ def _mirror_data_from_stormpath_account(self, account): if field != 'password': self.__setattr__(field, account[field]) for key in account.custom_data.keys(): - self.__setattr__(key.split(self.DJANGO_PREFIX)[0], account.custom_data[key]) + field_name = [part for part in key.split(self.DJANGO_PREFIX) if part][0] + value = account.custom_data[key] + # check if value is not None for nullable fields + if value: + if field_name in self.DATE_FIELDS: + value = parse_date(value) + elif field_name in self.DATETIME_FIELDS: + value = parse_datetime(value) + elif field_name in self.TIME_FIELDS: + value = parse_time(value) + self.__setattr__(field_name, value) if account.status == account.STATUS_ENABLED: self.is_active = True @@ -283,19 +306,10 @@ def _update_stormpath_user(self, data, raw_password): finally: self._remove_raw_password() - def get_full_name(self): - return "%s %s" % (self.given_name, self.surname) - - def get_short_name(self): - return self.email - - def __unicode__(self): - return self.get_full_name() - def _update_for_db_and_stormpath(self, *args, **kwargs): try: with transaction.atomic(): - super(StormpathBaseUser, self).save(*args, **kwargs) + super(StormpathMixin, self).save(*args, **kwargs) self._update_stormpath_user(model_to_dict(self), self._get_raw_password()) except StormpathError: raise @@ -308,7 +322,7 @@ def _update_for_db_and_stormpath(self, *args, **kwargs): def _create_for_db_and_stormpath(self, *args, **kwargs): try: with transaction.atomic(): - super(StormpathBaseUser, self).save(*args, **kwargs) + super(StormpathMixin, self).save(*args, **kwargs) account = self._create_stormpath_user(model_to_dict(self), self._get_raw_password()) self.href = account.href self.username = account.username @@ -324,7 +338,7 @@ def _create_for_db_and_stormpath(self, *args, **kwargs): raise def _save_db_only(self, *args, **kwargs): - super(StormpathBaseUser, self).save(*args, **kwargs) + super(StormpathMixin, self).save(*args, **kwargs) def _remove_raw_password(self): """We need to send a raw password to Stormpath. After an Account is saved on Stormpath @@ -368,7 +382,7 @@ def save(self, *args, **kwargs): def delete(self, *args, **kwargs): with transaction.atomic(): href = self.href - super(StormpathBaseUser, self).delete(*args, **kwargs) + super(StormpathMixin, self).delete(*args, **kwargs) try: account = APPLICATION.accounts.get(href) account.delete() @@ -376,8 +390,28 @@ def delete(self, *args, **kwargs): raise +class StormpathBaseUser(StormpathMixin, AbstractBaseUser, PermissionsMixin): + username = models.CharField(max_length=255, unique=True) + middle_name = models.CharField(max_length=255, null=True, blank=True) + + is_admin = models.BooleanField(default=False) + + class Meta: + abstract = True + + def get_full_name(self): + return "%s %s" % (self.given_name, self.surname) + + def get_short_name(self): + return self.email + + def __unicode__(self): + return self.get_full_name() + + class StormpathUser(StormpathBaseUser): - pass + class Meta(StormpathBaseUser.Meta): + swappable = 'AUTH_USER_MODEL' @receiver(pre_save, sender=Group) diff --git a/django_stormpath/social.py b/django_stormpath/social.py index ca6c1de..8bca22d 100644 --- a/django_stormpath/social.py +++ b/django_stormpath/social.py @@ -2,6 +2,7 @@ from django.shortcuts import resolve_url from django.core.urlresolvers import reverse from django.conf import settings +from django.utils.module_loading import import_string from stormpath.error import Error as StormpathError @@ -78,12 +79,12 @@ def get_access_token(provider, authorization_response, redirect_uri): def handle_social_callback(request, provider): provider_redirect_url = 'stormpath_' + provider.lower() + '_login_callback' abs_redirect_uri = request.build_absolute_uri( - reverse(provider_redirect_url, kwargs={'provider': provider})) + reverse(provider_redirect_url, kwargs={'provider': provider})) access_token = get_access_token( - provider, - request.build_absolute_uri(), - abs_redirect_uri) + provider, + request.build_absolute_uri(), + abs_redirect_uri) if not access_token: raise RuntimeError('Error communicating with Autentication Provider: %s' % provider) @@ -108,6 +109,17 @@ def handle_social_callback(request, provider): account = APPLICATION.get_provider_account(**params) + if hasattr(settings, 'STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA'): + if settings.STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA.get(provider): + update_custom_data_func = import_string( + settings.STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA.get(provider) + ) + + new_custom_data = update_custom_data_func(account, access_token) + for key, value in new_custom_data.items(): + # account doesn't have .update method + account.custom_data[key] = value + user = _get_django_user(account) user.backend = SOCIAL_AUTH_BACKEND django_login(request, user)