I have a CustomUser
model, and a Retailer
model that holds the additional details of retailer user type. The Retailer
model has a OneToOne relation to CustomUser
model. There is no public user registration or signup, accounts are created by superuser.
In the Django admin site, I am trying to leverage admin.StackedInline in the retailer admin page to enable superusers to create new retailer users directly from the retailer admin page. This eliminates the need to create a new user object separately in the CustomUser
model admin page and then associate it with a retailer object using the default dropdown in the retailer model admin page.
However, I got the below error:
MODELS.PY
class CustomUser(AbstractUser):
"""
Extended custom user model.
"""
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
username = None # type: ignore
first_name = None # type: ignore
last_name = None # type: ignore
name = models.CharField(_("Name of User"), max_length=150)
email = models.EmailField(_("Email address of User"), unique=True, blank=False)
date_modified = models.DateTimeField(auto_now=True)
# Flags for user types
is_retailer = models.BooleanField(
_("Retailer status"),
default=False,
help_text=_("Designates whether the user should treated as retailer"),
)
is_shop_owner = models.BooleanField(
_("Shop owner status"),
default=False,
help_text=_("Designates whether the user should treated as shop owner"),
)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["name"]
class Retailer(BaseModelMixin):
"""
Define retailer's profile.
"""
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="retailer")
phone = models.PositiveBigIntegerField(default=0, blank=True)
address = models.TextField(max_length=512, default="", blank=True)
google_map = models.URLField(max_length=1024, default="", blank=True)
slug = AutoSlugField(populate_from="user__name")
def __str__(self):
return self.user.name
ADMIN.PY
class UserInline(admin.StackedInline):
model = User
fields = ["name", "email", "password1", "password2"]
fk_name = "user"
extra = 0
@admin.register(Retailer)
class RetailerAdmin(BaseAdmin):
"""
Admin interface for the Retailer model
This class is inherited from the BaseAdmin class to include common fields.
"""
inlines = [UserInline]
fieldsets = (
(
None,
{"fields": ("user", "phone", "address", "google_map")},
),
)
list_display = [
"user",
"phone",
"created_at",
"updated_at",
]
Edit
I changed the fk_name
attribute of UserInline
class to "retailer", but I'm still getting an error saying 'accounts.CustomUser' has no field named 'retailer'. Where did I go wrong or am I missing something?
I discovered a convenient method to add retailer users from the Retailer
admin page as inline with the help of django_reverse_admin. Now, there's no need to create a new user separately in CustomUser
and associate it with a Retailer
using a dropdown. Additionally, I extended the UserCreationForm to set is_retailer
user type flag, making it easier to differentiate users in the CustomUser
model. You can the check the MRE here.
Updated ADMIN.PY
from django.contrib import admin
from django.contrib.auth.forms import UserCreationForm
from django_reverse_admin import ReverseModelAdmin, ReverseInlineModelAdmin
from .models import Retailer
class CustomReverseInlineModelAdmin(ReverseInlineModelAdmin):
"""
By overriding ReverseInlineModelAdmin and setting can_delete to False in the formset, the delete
checkbox is hidden, preventing the deletion of the Retailer object and its parent object in the User
model when using the UserCreationForm as the form in inline_reverse attribute of RetailerAdmin class.
Otherwise, If an attempt is made to delete the retailer model object by checking the checkbox and
hitting save button without providing a new password in both the password and password confirmation fields
(similar to when creating new user, which can be inconvenient in this case), a validation ValueError will
be raised by UserCreationForm with the message "The User could not be changed because the data didn't validate".
"""
def get_formset(self, request, obj=None, **kwargs):
formset = super().get_formset(request, obj, **kwargs)
formset.can_delete = False
return formset
class RetailerUserCreationForm(UserCreationForm):
"""
Extends UserCreationForm to set the is_retailer attribute to True in CustomUser model
upon saving a new retailer user, effectively identifying them as retailer user type.
"""
def save(self, commit=True):
user = super().save(commit=False)
user.is_retailer = True
user.save()
return user
@admin.register(Retailer)
class RetailerAdmin(ReverseModelAdmin):
inline_type = "stacked"
inline_reverse = [
(
"user",
{
"form": RetailerUserCreationForm,
"fields": ["name", "email", "password1", "password2"],
},
),
]
fieldsets = (
(
None,
{"fields": ("phone", "address", "google_map")},
),
)
list_display = [
"user",
"phone",
]
def get_inline_instances(self, request, obj=None):
"""
Overrides the get_inline_instances method to use CustomReverseInlineModelAdmin class for reverse inline instances.
"""
inline_instances = super().get_inline_instances(request, obj)
for inline in inline_instances:
if isinstance(inline, ReverseInlineModelAdmin):
inline.__class__ = CustomReverseInlineModelAdmin
return inline_instances
Although this is a useful approach when we have several user types/roles, I encountered two challenges:
When updating or changing a retailer user, I am required to enter a new password, as different forms cannot be used with the inline_reverse
attribute. Using UserChangeForm
could have resolved this issue.
When deleting a retailer user object, its related object from the CustomUser model is not automatically deleted. While setting the can_delete attribute to true can address this issue, it leads to a validation error due to the use of UserCreationForm.
Any assistance in resolving these two challenges would be greatly appreciated.