typeddjango / django-stubs

PEP-484 stubs for Django
MIT License
1.6k stars 450 forks source link

"AbstractBaseUser" has no attribute "is_staff" | With custom user model. #1353

Closed adambirds closed 3 months ago

adambirds commented 1 year ago

Bug report

What's wrong

Getting the below error in my views:

"AbstractBaseUser" has no attribute "is_staff"

I use a custom user model, see below:

models.py:

import uuid
from typing import Any

from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.models import UserManager as DefaultUserManager
from django.core.mail import send_mail
from django.core.validators import validate_email
from django.db import models
from django.utils.translation import gettext_lazy as _

class UserManager(BaseUserManager["User"]):
    use_in_migrations = True

    def _create_user(
        self,
        email: str,
        password: str,
        first_name: str,
        last_name: str,
        **extra_fields: dict[str, Any],
    ) -> "User":
        if not email:
            raise ValueError(_("The Email must be set"))
        if not first_name:
            raise ValueError(_("The First Name must be set"))
        if not last_name:
            raise ValueError(_("The Last Name must be set"))

        email = self.normalize_email(email)

        user = self.model(email=email, first_name=first_name, last_name=last_name, **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

    def create_user(
        self,
        email: str,
        password: str,
        first_name: str,
        last_name: str,
        **extra_fields: Any,
    ) -> "User":
        extra_fields.setdefault("is_staff", False)
        extra_fields.setdefault("is_superuser", False)
        return self._create_user(email, password, first_name, last_name, **extra_fields)

    def create_superuser(
        self,
        email: str,
        password: str,
        first_name: str,
        last_name: str,
        **extra_fields: Any,
    ) -> "User":
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        if extra_fields.get("is_staff") is not True:
            raise ValueError(_("Superuser must have is_staff=True."))
        if extra_fields.get("is_superuser") is not True:
            raise ValueError(_("Superuser must have is_superuser=True."))

        return self._create_user(email, password, first_name, last_name, **extra_fields)

    with_perm = DefaultUserManager.with_perm

class User(AbstractBaseUser, PermissionsMixin):  # type: ignore
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(
        verbose_name=_("email address"),
        unique=True,
        error_messages={
            "unique": _("A user with that email already exists."),
        },
        help_text=_("Required. 150 characters or fewer. Please enter a valid email address."),
        validators=[validate_email],
    )
    first_name = models.CharField(verbose_name=_("first name"), max_length=150, blank=False)
    last_name = models.CharField(verbose_name=_("last name"), max_length=150, blank=False)
    is_staff = models.BooleanField(
        verbose_name=_("staff status"),
        default=False,
        help_text=_("Designates whether the user can log into this admin site."),
    )
    is_active = models.BooleanField(
        verbose_name=_("active"),
        default=True,
        help_text=_(
            "Designates whether this user should be treated as active. "
            "Unselect this instead of deleting accounts."
        ),
    )
    date_joined = models.DateTimeField(verbose_name=_("date joined"), auto_now_add=True)

    objects = UserManager()

    EMAIL_FIELD: str = "email"
    USERNAME_FIELD: str = "email"
    REQUIRED_FIELDS: list[str] = ["first_name", "last_name"]

    class Meta:
        verbose_name = _("user")
        verbose_name_plural = _("users")

    def clean(self) -> None:
        super().clean()
        self.email = self.__class__.objects.normalize_email(self.email)

    def get_full_name(self) -> str:
        return f"{self.first_name} {self.last_name}".strip()

    def get_short_name(self) -> str:
        return self.first_name

    def email_user(self, subject: str, message: str, from_email: str = None, **kwargs: Any) -> None:
        send_mail(subject, message, from_email, [self.email], **kwargs)

views.py:

from typing import Any, Dict, Type

from django.contrib.auth import authenticate, login
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import AbstractBaseUser
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.views.generic import TemplateView

from apps_public.realms.custom_request import TenantHttpRequest
from apps_public.realms.models import Realm
from apps_tenants.ticket_system.forms import LoginForm
from apps_tenants.ticket_system.mixins import CustomerMixin, StaffMixin
from apps_tenants.ticket_system.models import Ticket

class StaffLoginView(TemplateView):
    template_name = "dashboard/dash-staff/login.html"
    form_class = LoginForm

    request: TenantHttpRequest

    def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
        context = super(StaffLoginView, self).get_context_data(**kwargs)

        context["logo_url"] = Realm.objects.get(
            schema_name=self.request.tenant.schema_name
        ).logo_url

        return context

    def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
        if request.user.is_authenticated:
            if request.user.is_staff:
                return redirect("staff-dashboard")
            else:
                return redirect("customer-dashboard")
        else:
            form = self.form_class(None)
            message = ""
            return render(request, self.template_name, {"form": form, "message": message})

    def post(self, request: HttpRequest) -> HttpResponse:
        form = self.form_class(request.POST)
        message = ""
        if form.is_valid():
            email = form.cleaned_data["email"]
            password = form.cleaned_data["password"]
            user = authenticate(username=email, password=password)
            if user is not None:
                if user.is_active:
                    if user.is_staff:
                        login(request, user)
                        return redirect("staff-dashboard")
                    else:
                        login(request, user)
                        return redirect("customer-dashboard")
                else:
                    message = "Your account has been disabled."
            else:
                message = "Invalid login"
        return render(request, self.template_name, {"form": form, "message": message})

settings.py:

AUTH_USER_MODEL = "authentication.User"

How is that should be

There shouldn't be this error.

System information

adambirds commented 1 year ago

Have alost tested with mypy 0.991 and django-stubs 1.14.0 too and still an issue.

christianbundy commented 1 year ago

I'm using a similar pattern and haven't bumped into this problem -- one difference is that my User is a subclass of AbstractUser rather than AbstractBaseUser, does changing that mitigate the issue?

adambirds commented 1 year ago

Unfortunately its impossible for me to change now and there were several reasons originally why I had to use the AbstractBaseUser instead but I can't remember why now.