From 07ead6e76f4db2eda9d005163f397f15bd85df20 Mon Sep 17 00:00:00 2001 From: Mo'men AbdElKader Date: Tue, 8 Mar 2016 15:09:01 +0200 Subject: [PATCH 1/4] Change import to get_user_model to direct import to StormpathUser --- django_stormpath/forms.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 " From 05d4da8f9f1cba4db106b28fdef3edd3be3ab409 Mon Sep 17 00:00:00 2001 From: Mo'men AbdElKader Date: Tue, 8 Mar 2016 15:50:45 +0200 Subject: [PATCH 2/4] add swappable = 'AUTH_USER_MODEL' attribute in StormpathUser model meta options --- django_stormpath/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_stormpath/models.py b/django_stormpath/models.py index 636f944..312bbcf 100644 --- a/django_stormpath/models.py +++ b/django_stormpath/models.py @@ -377,7 +377,8 @@ def delete(self, *args, **kwargs): class StormpathUser(StormpathBaseUser): - pass + class Meta(StormpathBaseUser.Meta): + swappable = 'AUTH_USER_MODEL' @receiver(pre_save, sender=Group) From 1c7a984cb90cc9af635d34741c389f6d3465e32a Mon Sep 17 00:00:00 2001 From: Mo'men AbdElKader Date: Wed, 27 Apr 2016 12:48:33 +0200 Subject: [PATCH 3/4] Enhance django_stormpath to be reusable in project with different user model - define StormpathMixin, containing all stormpath required fields and methods - ensure to serialize date, datetime, and time fields before sending to stormpath --- README.rst | 13 +++++ django_stormpath/backends.py | 2 +- django_stormpath/models.py | 106 +++++++++++++++++++++++------------ django_stormpath/social.py | 20 +++++-- 4 files changed, 99 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index 1a5e145..dbf90c6 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 resoponsible 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/models.py b/django_stormpath/models.py index 312bbcf..37869aa 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,16 @@ 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] + if field_name in self.DATE_FIELDS: + value = parse_date(account.custom_data[key]) + elif field_name in self.DATETIME_FIELDS: + value = parse_datetime(account.custom_data[key]) + elif field_name in self.TIME_FIELDS: + value = parse_time(account.custom_data[key]) + else: + value = account.custom_data[key] + self.__setattr__(field_name, value) if account.status == account.STATUS_ENABLED: self.is_active = True @@ -283,19 +305,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 +321,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 +337,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 +381,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,6 +389,25 @@ 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): class Meta(StormpathBaseUser.Meta): swappable = 'AUTH_USER_MODEL' 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) From 6647d9d8ed78a933e1508ea9296c411cf5d9f342 Mon Sep 17 00:00:00 2001 From: Mo'men AbdElKader Date: Sun, 15 May 2016 13:42:46 +0200 Subject: [PATCH 4/4] check if date/time fields value is None, to support nullable fields. --- README.rst | 2 +- django_stormpath/models.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 53e44f9..7e4e6ac 100644 --- a/README.rst +++ b/README.rst @@ -133,7 +133,7 @@ by defining STORMPATH_RETRIEVE_SOCIAL_CUSTOM_DATA variable like this: 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 resoponsible for connecting to the provider and process the data. +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/models.py b/django_stormpath/models.py index 37869aa..d6edb47 100644 --- a/django_stormpath/models.py +++ b/django_stormpath/models.py @@ -242,14 +242,15 @@ def _mirror_data_from_stormpath_account(self, account): self.__setattr__(field, account[field]) for key in account.custom_data.keys(): field_name = [part for part in key.split(self.DJANGO_PREFIX) if part][0] - if field_name in self.DATE_FIELDS: - value = parse_date(account.custom_data[key]) - elif field_name in self.DATETIME_FIELDS: - value = parse_datetime(account.custom_data[key]) - elif field_name in self.TIME_FIELDS: - value = parse_time(account.custom_data[key]) - else: - value = account.custom_data[key] + 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: