Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
adambirds opened this issue Jan 31, 2023 · 3 comments · Fixed by #2335
Closed

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

adambirds opened this issue Jan 31, 2023 · 3 comments · Fixed by #2335
Labels
bug Something isn't working

Comments

@adambirds
Copy link
Contributor

adambirds commented Jan 31, 2023

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

  • OS:
  • python version: 3.10.6
  • django version: 4.1.5
  • mypy version: 0.931
  • django-stubs version: 1.12.0
  • django-stubs-ext version: 0.7.0
@adambirds adambirds added the bug Something isn't working label Jan 31, 2023
@adambirds
Copy link
Contributor Author

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

@christianbundy
Copy link
Contributor

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
Copy link
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Development

Successfully merging a pull request may close this issue.

2 participants