Hi there.
I am trying to understand if using signals to extend the built-in User model is a good practice in term of design.
Thanks
John
Hi there.
I am trying to understand if using signals to extend the built-in User model is a good practice in term of design.
Thanks
John
There are multiple valid and different opinions about this.
My perspective is just one of them.
I only use signals when I have absolutely no other option to do what needs to be done. They are always my last choice as an architectural decision.
So to address your question directly, I would say it might be ok to use a signal, depending upon why you’re using it.
That’s just my opinion. Other people have other opinions, and in this case, it would be valid for them.
I started with Signals to extend my user model, then switched to extending the base user model. Now personally, based upon the amounts of times the auth request model is called and wanting to avoid bloat in it. I wish I never extended the base model, as my thinking is keep the model where auth occurs as small as possible and just have a separate user profile which can have a 1:1 relationship.
This can be handled via signals to automatically create profile and create the relationship. You could overwrite default save method, but again, I try to avoid modifying anything that is used by Authentication system.
But like Ken said, its really personal preference and how you project needs to be structured.
Hi,
I just finished extending the built-in User model by creating a (user) Profile using signals because I wanted to add avatar upload functionality. I’m really liking it, and as @mast3rbow said, it is nice to separate the built-in user from the profile. But then, that is just my opinion.
My most serious concern with using signals in this situation is that there are multiple ways in which a User object can be created without firing the signal. If this occurs, then you’re not going to know about this until you try to reference the profile for such a User, and the system throws an error.
On the other hand, if you build your system such that a reference to your profile uses get_or_create
to retrieve the Profile, then it doesn’t matter how the User was created. Your system will never throw an error as a result of it.
However, whether or not you will ever encounter that situation is a different, and valid, question. You may be working in an environment in which you have a reasonable expectation of it not happening - in which case you’re ok.
Interesting. It is something worth looking into, but I am going to stick with what I set up for now. When I have a moment I will share the code here. Thanks @KenWhitesell!
Hi,
As promised, here is the code for the extended user profile using signals:
# accounts/models.py
from django.db import models
from django.contrib.auth.models import User
from PIL import Image
# Extending User Model Using a One-To-One Link
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
avatar = models.ImageField(default='default.jpg', upload_to='profile_images')
bio = models.TextField()
def __str__(self):
return self.user.username
# resizing images
def save(self, *args, **kwargs):
super().save()
img = Image.open(self.avatar.path)
if img.height > 80 or img.width > 80:
new_img = (80, 80)
img.thumbnail(new_img)
img.save(self.avatar.path)
def __str__(self):
return self.user.username
# accounts/forms.py
# The signup form and UpdateUserForm were done previously. The UpdateProfileForm was done when I created the extended user Profile.
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Profile
class SignUpForm(UserCreationForm):
email = forms.CharField(max_length=254, required=True, widget=forms.EmailInput())
class Meta:
model = User
fields = ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')
class UpdateUserForm(forms.ModelForm):
username = forms.CharField(max_length=100,
required=True,
widget=forms.TextInput(attrs={'class': 'form-control'}))
email = forms.EmailField(required=True,
widget=forms.TextInput(attrs={'class': 'form-control'}))
class Meta:
model = User
fields = ['username', 'email']
class UpdateProfileForm(forms.ModelForm):
avatar = forms.ImageField(widget=forms.FileInput(attrs={'class': 'form-control-file'}))
bio = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 5}))
class Meta:
model = Profile
fields = ['avatar', 'bio']
# accounts/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
# accounts/views.py
from django.contrib.auth import login as auth_login
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views.generic.edit import UpdateView
from django.views.generic import ListView
from django.views.generic.detail import DetailView
from django.shortcuts import get_object_or_404
from .models import Profile
from django.contrib import messages
from .forms import SignUpForm, UpdateUserForm, UpdateProfileForm
def signup(request):
if request.method == 'POST':
form = SignUpForm(request.POST)
if form.is_valid():
user = form.save()
auth_login(request, user)
return redirect('index')
else:
form = SignUpForm()
return render(request, 'signup.html', {'form': form})
@method_decorator(login_required, name='dispatch')
class UserUpdateView(UpdateView):
model = User
fields = ('first_name', 'last_name', 'email', )
template_name = 'my_account.html'
success_url = reverse_lazy('my_account')
def get_object(self):
return self.request.user
@login_required
def profile(request):
if request.method == 'POST':
user_form = UpdateUserForm(request.POST, instance=request.user)
profile_form = UpdateProfileForm(request.POST, request.FILES, instance=request.user.profile)
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(request, 'Your profile is updated successfully')
return redirect(to='users-profile')
else:
user_form = UpdateUserForm(instance=request.user)
profile_form = UpdateProfileForm(instance=request.user.profile)
return render(request, 'users/profile.html', {'user_form': user_form, 'profile_form': profile_form})
@method_decorator(login_required, name='dispatch')
class ProfileListView(ListView):
model = Profile
context_object_name = 'profiles'
template_name = 'users/profiles.html'
# paginates profiles.html
paginate_by = 3
@login_required
def profile_detail(request, pk):
profile = get_object_or_404(Profile, pk=pk)
return render(request, 'users/profile_detail.html', {'profile': profile})
# accounts/urls.py
from django.urls import path
from .views import profile, profile_detail, ProfileListView, UserUpdateView
urlpatterns = [
path('account/', UserUpdateView.as_view(), name='my_account'),
path("profile/", profile, name="users-profile"),
path('profile-detail/<int:pk>/', profile_detail, name='profile'),
path("profiles/", ProfileListView.as_view(), name="users-profile-list"),
]
There are also templates of course and tests, but that would just get to be too long! The tests alll pass, and the templates all work properly. If however, you are interested in seeing that too, I will then share it. I also had to do some finagling as regards what avatar and how I implemented it in my boards app topic posts vs the extended user profile post. That is why I ended up creating a list of user profiles for everyone to see with truncated bio, as well as a profile detail page where the complete bio was available for viewing.