0
votes

I am trying to implement django-allauth with a custom user model, but I keep getting errors when a new user is registering. The following is my User model:

# models.py
class User(AbstractUser):
   name = models.CharField(_("Name of User"), blank=True, max_length=255)
   tier = models.CharField(_("Plan"), choices=TIER_CHOICES, default='FREE', max_length=5)
   dateOfBirth = models.DateField(_("Date of birth"), auto_now=False, auto_now_add=False)
   gender = models.CharField(_('Gender'), choices=GENDER_CHOICES, default='MASC', max_length=5)
   city = models.CharField(_("City"), max_length=100)

The fields on the model are to be completed by the user on Signup. To do this I have written a Custom Signup Form class, as per these instructions, which indicated the need for a signup() method:

# forms.py
class CustomSignupForm(SignupForm):
   name = f.CharField(label=_("First Name"), 
                   required=True, 
                   widget=f.TextInput(attrs={'placeholder': _("First Name")})
                   )
   dateOfBirth = f.DateField(label=_("Date of birth"),
                          required=True, initial='1990-01-01',
                          widget=f.DateInput()
                          )
   city = f.CharField(label=_("City"), 
                   required=True, 
                   widget=f.TextInput(attrs={'placeholder': _("City")})
                   )
   gender = f.ChoiceField(label=_("Gender"), 
                       choices=GENDER_CHOICES, 
                       required=True
                       )

   def signup(self, request, user):
      user.name = self.cleaned_data['name']
      user.dateOfBirth = self.cleaned_data['dateOfBirth']
      user.city = self.cleaned_data['city']
      user.gender = self.cleaned_data['gender']
      user.save()
      return user

The same instructions also informed me I needed a custom allauth adapter (which is the basic allauth one but with the added fields such as dateOfBirth and city):

# adapter.py
class AccountAdapter(DefaultAccountAdapter):
   def save_user(self, request, user, form, commit=False):
       data = form.cleaned_data
       user.email = data['email']
       user.name = data['name']
       user.dateOfBirth = data['dateOfBirth']
       user.gender = data['gender']
       user.city = data['city']
       if 'password1' in data:
           user.set_password(data['password1'])
       else:
           user.set_unusable_password()
       self.populate_username(request, user)
       if commit:
           user.save()
       return user

With this setup, I keep getting the same error (traceback):

save() prohibited to prevent data loss due to unsaved related object 'user'.

I have found a possible solution to this here and here, which is to add the save() method to the custom signup form:

# forms.py
class CustomSignupForm(SignupForm):
   # ...
   def save(self, user):
       user.save()

When I try this, I get the following error (traceback):

AttributeError at /accounts/signup/ 'WSGIRequest' object has no attribute 'save'

I have also tried other solutions such as this. If anyone can point me in the right direction I would be grateful!

My settings.py config for django-allauth:

# settings.py (django-allauth)

ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True)

ACCOUNT_AUTHENTICATION_METHOD = "email"

ACCOUNT_EMAIL_REQUIRED = True

ACCOUNT_EMAIL_VERIFICATION = "mandatory"

ACCOUNT_ADAPTER = "loteriai.users.adapters.AccountAdapter"

SOCIALACCOUNT_ADAPTER = "loteriai.users.adapters.SocialAccountAdapter"

ACCOUNT_FORMS = {'signup': 'loteriai.users.forms.CustomSignupForm'}

I am using the following packages:

Django Version: 3.1
Python Version: 3.8.3
django-allauth Version: 0.42.0
1

1 Answers

0
votes

I managed to work around this issue. Django-Allauth apparently has an issue with custom fields in the User model other than char fields.

My solution was to use a separate User_Profile model that has a OneToOne relationship with the User model and all the necessary additional fields:

# Models.py
   class User_Profile(models.Model):
       user = models.OneToOneField(User, verbose_name=_("user"),
                                         related_name="profile",
                                         on_delete=models.CASCADE)
       tier = models.CharField(_("Plan"), choices=TIER_CHOICES,
                                          default='FREE',
                                          max_length=5)
       dateOfBirth = models.DateField(_("Date of birth"),
                                      auto_now=False,
                                      auto_now_add=False,
                                      null=True)
       gender = models.CharField(_('Gender'), choices=GENDER_CHOICES,
                                              default='MASC',
                                              max_length=5,
                                              null=True)
       city = models.CharField(_("City"), max_length=100,
                                          null=True)

Make sure you have the fields that do not default to anything set to null=True as this will be important in the code flow below.

Next, for the custom form you need a save method instead of a signup method. I found this out in this article. Apparently the signup method is deprecated. More info on the official docs. I ended up with the following CustomSignupForm class:

   # forms.py
   class CustomSignupForm(SignupForm):
       name = f.CharField(label=_("First Name"),
                          required=True,
                          widget=f.TextInput(attrs={'placeholder': _("First Name")}))
       dateOfBirth = f.DateField(label=_("Date of birth"), required=True,
                                                           initial='1990-01-01',
                                                           widget=f.DateInput())
       city = f.CharField(label=_("City"),
                          required=True,
                          widget=f.TextInput(attrs={'placeholder': _("City")}))
       gender = f.ChoiceField(label=_("Gender"), choices=GENDER_CHOICES, required=True)

       def save(self, request):
           user = super(CustomSignupForm, self).save(request)
           user.name = self.cleaned_data['name']
           user.save()

           profile = user.profile
           profile.dateOfBirth = self.cleaned_data['dateOfBirth']
           profile.city = self.cleaned_data['city']
           profile.gender = self.cleaned_data['gender']
           profile.save()

           return user

Finally, you need to create a signal to create or update the profile model whenever the User model is created/updated:

# signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import User, User_Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
   if created:
       User_Profile.objects.create(user=instance)
   else:
       try:
           instance.profile.save()
       except User.profile.RelatedObjectDoesNotExist:
           # Run this when a profile in not found for a User instance

Hopefully this has been helpful!