4

I am attempting to work out how to extend the Django user model to add information to a user. I can't seem to get it to work. What am I doing wrong? Is it okay to have foreignkeys within the same model I am extending in to? How do you create a superuser, or do you have to do it manually through the python manage.py shell?

Here's my code so far:

class PersonModel(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    firstName = models.CharField(max_length=50)
    lastName = models.CharField(max_length=50)
    company = models.ForeignKey(CompanyModel, on_delete=models.CASCADE, null=True)
    phone = models.ForeignKey(PhoneModel, on_delete=models.CASCADE, null=True)
    email = models.EmailField(blank=True)

    def __str__(self):
        return '%s %s - %s - %s, %s' % (self.firstName, self.lastName,
                                        self.company, self.phone, self.email
                                       )

    class Meta:
        ordering = ['firstName']
        verbose_name = "Customer Contact Information"
        #verbose_name_plural = "Contacts"

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        PersonModel.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

UPDATE (Final): With the help of raratiru I've been able to mostly get the script he shared going. I still struggle to create a super user because of my foreign key requirements.

from django.contrib.auth.models import (
    AbstractBaseUser,
    PermissionsMixin,
    BaseUserManager,
)
from django.core.mail import send_mail
from django.db import models
from django.utils.translation import ugettext_lazy as _
from customers import models as customers_models

class TravelModel(models.Model):
   mileageRate = models.DecimalField(max_digits=4, decimal_places=3)

   def __str__(self):
      return '%s' % (self.mileageRate)

   class Meta:
      verbose_name = "Current Federal Milage Rate"
      #verbose_name_plural = "Milage"


class UserManager(BaseUserManager):
    def create_user(self, email, firstName, lastName, company, phone, password=None, **kwargs):
        email = self.normalize_email(email)
        user = self.model(email=email, **kwargs)
        user.firstName = firstName
        user.lastName = lastName
        user.company = company
        user.phone = phone
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, firstName, lastName, password=None, **kwargs):
        #user = self.create_user(**kwargs)
        email = self.normalize_email(email)
        user = self.model(email=email, **kwargs)
        user.firstName = firstName
        user.lastName = lastName
        user.set_password(password)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)
        return user


class AliasField(models.Field):
    def contribute_to_class(self, cls, name, virtual_only=False):
        super().contribute_to_class(cls, name, virtual_only=True)
        setattr(cls, name, self)

    def __get__(self, instance, instance_type=None):
        return getattr(instance, self.db_column)


class MyUser(AbstractBaseUser, PermissionsMixin):
      firstName = models.CharField(max_length=50, blank=False, null=False)
      lastName = models.CharField(max_length=50, blank=False, null=False)
      company = models.ForeignKey(customers_models.CompanyModel, on_delete=models.PROTECT, null=False)
      phone = models.ForeignKey(customers_models.PhoneModel, on_delete=models.PROTECT, null=False)

      email = models.EmailField(_('email address'), max_length=255, unique=True)

      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.'
            )
      )
      username = AliasField(db_column='email')

      objects = UserManager()

      USERNAME_FIELD = 'email'
      REQUIRED_FIELDS = ['firstName','lastName',]

      class Meta(object):
            ordering = ['firstName']
            verbose_name = _('Contact')
            verbose_name_plural = _('Contacts')

      def __str__(self):
            return '%s - %s %s - %s - %s' % (self.company, self.firstName, self.lastName, self.email, self.phone)

      def get_full_name(self):
            return self.email

      def get_short_name(self):
            return self.email

      def email_user(self, subject, message, from_email=None, **kwargs):
            """
            Sends an email to this User.
            """
            send_mail(subject, message, from_email, [self.email], **kwargs)

To circumvent the foreignkey struggles, the easiest solution is to remove the null=False requirement before creating the superuser - assign a company and phone after - and set null back to false afterwards.

addohm
  • 2,248
  • 3
  • 14
  • 40

1 Answers1

2

This is how I extended my user model. The following code also substitutes the username for an email field. I am posting it because it causes core changes so it makes clear the logic behind.

The base idea can be found and explained in this post. A very nice post can also be found here.

The AliasField in this case, creates the field username as an alias to email. Although this is not necessary given that django has documented the proper way of finding the user model and its relevant fields.

from django.contrib.auth.models import (
    AbstractBaseUser,
    PermissionsMixin,
    BaseUserManager,
)
from django.core.mail import send_mail
from django.db import models
from django.utils.translation import ugettext_lazy as _


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **kwargs):
        email = self.normalize_email(email)
        user = self.model(email=email, **kwargs)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, **kwargs):
        user = self.create_user(**kwargs)
        user.is_superuser = True
        user.is_staff = True
        user.save(using=self._db)
        return user


class AliasField(models.Field):
    def contribute_to_class(self, cls, name, private_only=False):
        super().contribute_to_class(cls, name, private_only=True)
        setattr(cls, name, self)

    def __get__(self, instance, instance_type=None):
        return getattr(instance, self.db_column)


class MyUser(AbstractBaseUser, PermissionsMixin):
    custom_field = models.ForeignKey(
        'app.Model',
        on_delete=models.PROTECT,
    )

    email = models.EmailField(_('email address'), max_length=255, unique=True)

    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.'
        )
    )
    username = AliasField(db_column='email')

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['user_branch_key', ]

    class Meta(object):
        ordering = ['email']
        verbose_name = _('My User')
        verbose_name_plural = _('My User')

    def __str__(self):
        return 'id: {0} - {1}, {2}'.format(self.id, self.email, self.user_branch_key)

    def get_full_name(self):
        return self.email

    def get_short_name(self):
        return self.email

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email], **kwargs)

Once you extend your user model, for example inside the application with the name the_user in the file ./the_user/models.py you have to make some changes in the settings.py file:

  • Register the application in the INSTALLED_APPS
  • ./manage.py makemigrations && ./manage.py migrate
  • Set the AUTH_USER_MODEL = 'the_user.MyUser as described in the docs

This way, in another model you can add a foreignkey as follows:

from the_user.models import MyUser
from django.conf import settings
from django.db import models

class AModel(models.Model)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name='%(app_label)s_%(class)s_user'
    )
raratiru
  • 8,748
  • 4
  • 73
  • 113
  • Seems like rather than extending, you're rewriting the django user model. That's a hell of a lot more elaborate than the Django docs on this subject :( – addohm Nov 11 '17 at 17:12
  • @Jaberwocky Indeed. I was searching for the code to make core changes (use email instead of the username) and the above illustrates the logic behind the user model. – raratiru Nov 11 '17 at 17:16
  • I appreciate your contribution. I feel like this doesn't answer my question fully though. Can you, and if so, how do you tie in foreign key relationships to the user? More elaborately, if I wanted to store what I have in `PersonModel`, how can that be done using your method? – addohm Nov 11 '17 at 17:23
  • I found the rest of my answers in https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#referencing-the-user-model I believe. Thanks! – addohm Nov 11 '17 at 17:42
  • @Jaberwocky Yes indeed. You can also add your model as an inline to the existing user admin instead of rewriting it as I did. I have updated my answer to illustrate how you can add foreign keys. – raratiru Nov 11 '17 at 17:54
  • Please do, that's the most critical part of this for me. – addohm Nov 11 '17 at 17:57
  • what is `user_branch_key = models.ForeignKey( 'ellas.Municipality',...`? – addohm Nov 11 '17 at 18:01
  • @Jaberwocky It is a custom field nothing more. It could be `phone = models.ForeignKey(PhoneModel, ...` – raratiru Nov 11 '17 at 18:02
  • Right, so isn't this where I would add fields to the user (incl. foreign keys)? Then tack them back up at the top in the create_user methods? – addohm Nov 11 '17 at 18:04
  • @Jaberwocky Yes, there you can add all the fields of the `PersonModel` except the `OneToOneField`. – raratiru Nov 11 '17 at 18:06
  • The OneToOne field was just my attempt at extending the built in user model :) – addohm Nov 11 '17 at 18:07
  • @Jaberwocky Yes a good attempt, which can be useful if you attach it as an inline to the existing user model. But sometimes it is a better and more robust solution to have it all together in one place. – raratiru Nov 11 '17 at 18:11
  • So when you foreignkey something, how do you create a superuser from a fresh environment? For example in the model I posted, there is no company or phone at the time of initial migration. – addohm Nov 11 '17 at 18:19
  • @Jaberwocky According to [the docs](https://docs.djangoproject.com/en/dev/topics/auth/customizing/#django.contrib.auth.models.CustomUser.REQUIRED_FIELDS), any field that is not null or blank, should be included in the `REQUIRED_FIELDS`. Of course, the fields you have created have `null=True` therefore they will be empty and they have to be manually filled. – raratiru Nov 11 '17 at 18:45
  • Sorry, again, that's old. I'm running off of your script now. I do have those items in required fields. I'll update. When I go to create a superuser, it askes me for a company ID. Obviously there are no companies yet. – addohm Nov 11 '17 at 18:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/158778/discussion-between-jaberwocky-and-raratiru). – addohm Nov 11 '17 at 19:01