Multiple User Types with Django


(Yurii Parfonov) #1

Hi everybody,

I’m developing a project like the “Django School” from excellent Vitor’s tutorial
“How to Implement Multiple User Types with Django”. It’s supposed to be three types of users in
my web application: superuser, teacher (staff member) and student.

I have a custom user model:

class CustomUser(AbstractUser):
    is_student = models.BooleanField(default=False, verbose_name='Student')
    is_teacher = models.BooleanField(default=False, verbose_name='Teacher')
    class Meta:
        db_table = "custom_users"

a custom student model with some extra information, related to all students:

class Student(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
    l_name = models.CharField(max_length=25, verbose_name='Student Last Name')
    f_name = models.CharField(max_length=25, verbose_name='Student First Name')
    m_name = models.CharField(max_length=25, verbose_name='Student Middle Name')
    year = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(6)], default=2, verbose_name='Year')
    academic_group = models.ForeignKey('AcademicGroup', verbose_name='Academic Group', on_delete=models.CASCADE)   
    class Meta:
        unique_together = ("l_name", "f_name", "m_name")
        db_table = "students"

and a form to sign up the student

class StudentSignUpForm(UserCreationForm):
    first_name = forms.CharField(max_length=30, required=True, label=_('First name'))
    middle_name = forms.CharField(max_length=30, required=True, label=_('Middle name'))
    last_name = forms.CharField(max_length=30, required=True, label=_('Last name'))
    year = forms.IntegerField(min_value=1, max_value=6, required=True, label=_('Year'))
    academic_group = forms.ModelChoiceField(queryset=AcademicGroup.objects.all().order_by('academic_group_code'),
                                            label=_('Academic group'))
    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('last_name', 'first_name', 'middle_name', 'year', 'academic_group',) + UserCreationForm.Meta.fields

    @transaction.atomic
    def save(self):
        user = super().save(commit=False)
        user.is_student = True
        user.save()
        student = Student.objects.create(user=user, academic_group=self.cleaned_data.get('academic_group'))
        student.f_name = self.cleaned_data.get('first_name')
        student.m_name = self.cleaned_data.get('middle_name')
        student.l_name = self.cleaned_data.get('last_name')
        student.year = self.cleaned_data.get('year')
		#1
        student.save()
		#1
        return user

I would like to store user data (username, password, …) in the database table “custom_users” and
data related to the student (f_name, m_name, l_name, year, academic_group) in the “students” table.

However, I have some difficulties doing that.
First, if #1 statement in the StudentSignUpForm is commented, only values of “year” and “academic_group” are in the “students” table.
First Name and Last Name goes to the “custom_users” table.
Second, if the #1 statement is uncommented, all data related to the student goes to the “students”
table, and “f_name” and “l_name” - to the “custom_users” table too.

I wonder if there is something wrong with my code or I need to pick a different strategy
to implement custom user model?

Thanks in advance.


(Callum Reid) #2

Hi Yurii,

As your CustomUser is an extension of AbstractUser, it already contains fields for first_name and last_name. Consequently, storing this information within the Student class is not ideal as you are duplicating information. I would recommend you store this all within the CustomUser class, like so:

class CustomUser(AbstractUser):
    is_student = models.BooleanField(default=False, verbose_name='Student')
    is_teacher = models.BooleanField(default=False, verbose_name='Teacher')
    first_name = models.CharField(_('First name'), max_length=30, blank=False)
    middle_name = models.CharField(_('Middle name'), max_length=30, blank=False)
    last_name = models.CharField(_('Last name'), max_length=30, blank=False)

    class Meta:
        db_table = "custom_users"
        unique_together = ("last_name", "first_name", "middle_name")

With these changes, your student model would become:

class Student(models.Model):
    user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, primary_key=True)
    year = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(6)], default=2, verbose_name='Year')
    academic_group = models.ForeignKey('AcademicGroup', verbose_name='Academic Group', on_delete=models.CASCADE)   
class Meta:
        db_table = "students"

Finally, your form:

class StudentSignUpForm(UserCreationForm):
    year = forms.IntegerField(min_value=1, max_value=6, required=True, label=_('Year'))
    academic_group = forms.ModelChoiceField(queryset=AcademicGroup.objects.all().order_by('academic_group_code'),
                                            label=_('Academic group'))
    class Meta(UserCreationForm.Meta):
        model = CustomUser
        fields = ('username',
                  'first_name',
                  'middle_name',
                  'last_name',
                  'year',
                  'academic_group',
        )

    @transaction.atomic
    def save(self):
        user = super(StudentSignUpForm, self).save(commit=False)
        user.is_student = True
        user.save()
        Student.objects.create(user=user,
                               academic_group=self.cleaned_data.get('academic_group'),
                               year=self.cleaned_data.get('year')
        )

        return user

Note: These changes to your CustomUser class would require all user types to input their first, middle, and last names. If this is not the desired functionality, you could remove the blank=False tags and instead include a form field for each name in the StudentSignUpForm with a required=True tag.


(Yurii Parfonov) #3

Hi Callum,
thank you for your advice. I think the solution is worth trying.


(Yurii Parfonov) #4

Hi there,
I’ve implemented Callum’s solution and it is working just fine.

In my opinion, another option is keeping all teachers’ and students’ profile data in the respective database tables and using model inheritance to decrease duplication of the information.
Here it is the code:

class CustomUser(AbstractUser):
is_student = models.BooleanField(default=False)
is_teacher = models.BooleanField(default=False)
first_name = None
last_name = None
class Meta:
db_table = “custom_users”

class CommonInfo(models.Model):
first_name = models.CharField(max_length=30, blank=False, default=’’)
middle_name = models.CharField(max_length=30, blank=False, default=’’)
last_name = models.CharField(max_length=30, blank=False, default=’’)
class Meta:
abstract = True
unique_together = (“last_name”, “first_name”, “middle_name”)
def str(self):
return ‘%s %s %s’ % (self.last_name, self.first_name, self.middle_name)

teacher profile

class Teacher(CommonInfo):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
TYPE_OF_POSITION = (
(‘SL’, ‘SENIOR LECTURER’),
(‘AP’, ‘ASSOCIATE PROFESSOR’),
(‘FP’, ‘FULL PROFESSOR’),
)
position = models.CharField(max_length=25, choices=TYPE_OF_POSITION, blank=False, default=‘AP’)
department = models.ForeignKey(‘Department’, null=True, on_delete=models.CASCADE)
class Meta:
db_table = “teachers”

student profile

class Student(CommonInfo):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
year = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(6)], default=2)
academic_group = models.ForeignKey(‘AcademicGroup’, on_delete=models.CASCADE)
class Meta(CommonInfo.Meta):
db_table = “students”

And I guess field “user” actually should be located in CommonInfo class. That’s it.