Django admin StackedInline throws (admin.E202) 'accounts.CustomUser' has no field named 'user' error

261 Views Asked by At

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:

enter image description here

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?

1

There are 1 best solutions below

0
On BEST ANSWER

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:

  1. 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.

  2. 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.