Search code examples
djangoserializationdjango-allauthgoogle-account

Django allauth Serialization error custom User model with TimeZoneField


My custom User model have a TimeZoneField:

from timezone_field import TimeZoneField


class User(AbstractBaseUser, PermissionsMixin):

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    email = models.EmailField(_('email address'), unique=True, blank=False, null=False)
    username = models.CharField(_('user name'), max_length=128, unique=True, blank=False, null=False)

    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)

    language = models.CharField(_('Language'), choices=settings.LANGUAGES, default=settings.ENGLISH, max_length=2)
    timezone = TimeZoneField(verbose_name=_('Timezone'), default='Europe/London')

    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

I use django-allauth for registration by Google accounts. When existing user (registered by google email before, not Google Account) trying login by Google Account we have error:

<DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD> is not JSON serializable


Traceback:

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  149.                     response = self.process_exception_by_middleware(e, request)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/django/core/handlers/base.py" in get_response
  147.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/providers/oauth2/views.py" in view
  55.             return self.dispatch(request, *args, **kwargs)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/providers/oauth2/views.py" in dispatch
  125.             return complete_social_login(request, login)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/helpers.py" in complete_social_login
  142.         return _complete_social_login(request, sociallogin)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/helpers.py" in _complete_social_login
  158.         ret = _process_signup(request, sociallogin)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/helpers.py" in _process_signup
  25.         request.session['socialaccount_sociallogin'] = sociallogin.serialize()

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/socialaccount/models.py" in serialize
  189.                    user=serialize_instance(self.user),

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/allauth/utils.py" in serialize_instance
  194.     return json.loads(json.dumps(data, cls=DjangoJSONEncoder))

File "/usr/lib/python3.4/json/__init__.py" in dumps
  237.         **kw).encode(obj)

File "/usr/lib/python3.4/json/encoder.py" in encode
  192.         chunks = self.iterencode(o, _one_shot=True)

File "/usr/lib/python3.4/json/encoder.py" in iterencode
  250.         return _iterencode(o, 0)

File "/webapps/myproject/tmp/venv/lib/python3.4/site-packages/django/core/serializers/json.py" in default
  115.             return super(DjangoJSONEncoder, self).default(o)

File "/usr/lib/python3.4/json/encoder.py" in default
  173.         raise TypeError(repr(o) + " is not JSON serializable")

Exception Type: TypeError at /accounts/google/login/callback/
Exception Value: <DstTzInfo 'Europe/London' LMT-1 day, 23:59:00 STD> is not JSON serializable

What are some ways to serialize a custom field in allauth?


Solution

  • My solution is replace default DefaultSocialAccountAdapter and extension serialize_instance (from allauth.utils) for serializing TimeZoneField. Don't forget set custom adapret in project settings:

    SOCIALACCOUNT_ADAPTER = 'myapp.adapter.MySocialAccountAdapter'
    

    Also I replaced pre_social_login for association Social account with Direct account (registered by email) (Thanks elssar for his example: https://stackoverflow.com/a/19443127/4012716)

    myapp.adapter.py:

    import json
    import base64
    import logging
    
    
    from django.db.models import FieldDoesNotExist, FileField
    from django.db.models.fields import (BinaryField)
    from django.utils import six
    from django.core.serializers.json import DjangoJSONEncoder
    from django.shortcuts import HttpResponse
    
    try:
        from django.utils.encoding import force_text
    except ImportError:
        from django.utils.encoding import force_unicode as force_text
    
    from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
    from allauth.account.adapter import DefaultAccountAdapter
    from allauth.utils import SERIALIZED_DB_FIELD_PREFIX
    from allauth.exceptions import ImmediateHttpResponse
    from timezone_field import TimeZoneField
    
    from accounts.models import User
    
    
    logger = logging.getLogger("django")
    
    
    def my_serialize_instance(instance):
        """Instance serializer supported of serialization of TimeZoneField.
        :param instance:
        :return:
        """
        data = {}
        for k, v in instance.__dict__.items():
            if k.startswith('_') or callable(v):
                continue
            try:
                field = instance._meta.get_field(k)
                if isinstance(field, BinaryField):
                    v = force_text(base64.b64encode(v))
                elif isinstance(field, FileField):
                    if not isinstance(v, six.string_types):
                        v = v.name
                elif isinstance(field, TimeZoneField):
                    v = six.text_type(v.zone)
                # Check if the field is serializable. If not, we'll fall back
                # to serializing the DB values which should cover most use cases.
                try:
                    json.dumps(v, cls=DjangoJSONEncoder)
                except TypeError:
                    v = field.get_prep_value(v)
                    k = SERIALIZED_DB_FIELD_PREFIX + k
            except FieldDoesNotExist:
                pass
            data[k] = v
        return json.loads(json.dumps(data, cls=DjangoJSONEncoder))
    
    
    class MySocialAccountAdapter(DefaultSocialAccountAdapter):
        """Custom SocialAccountAdapter for django-allauth.
        Replaced standard behavior for serialization of TimeZoneField.
    
        Need set it in project settings:
        SOCIALACCOUNT_ADAPTER = 'myapp.adapter.MySocialAccountAdapter'
        """
    
        def __init__(self, request=None):
            super(MySocialAccountAdapter, self).__init__(request=request)
    
        def pre_social_login(self, request, sociallogin):
            # This isn't tested, but should work
            try:
                emails = [email.email for email in sociallogin.email_addresses]
                user = User.objects.get(email__in=emails)
                sociallogin.connect(request, user)
                raise ImmediateHttpResponse(response=HttpResponse())
            except User.DoesNotExist:
                pass
            except Exception as ex:
                logger.error(ex)
    
        def serialize_instance(self, instance):
            return my_serialize_instance(instance)