Repository: imAsparky/django-cookiecutter

Issue:
[CHORE]: Refactor custom user model and manager.
**What is the chore?**
The custom user model and manager could be less cluttered and clearer to read.


Discussion:

Resolving patch:
diff --git a/{{cookiecutter.git_project_name}}/users/models.py b/{{cookiecutter.git_project_name}}/users/models.py
--- a/{{cookiecutter.git_project_name}}/users/models.py
+++ b/{{cookiecutter.git_project_name}}/users/models.py
@@ -1,38 +1,57 @@
 """{{cookiecutter.git_project_name}} project CustomUser Models."""
 
+from django.apps import apps
+from django.contrib import auth
+from django.contrib.auth.hashers import make_password
 from django.contrib.auth.models import AbstractUser
 from django.contrib.auth.base_user import BaseUserManager
 from django.utils.translation import gettext_lazy as _
 from django.db import models
-from django.utils import timezone
 
 
 class CustomUserManager(BaseUserManager):
-    """Custom user manager."""
+    use_in_migrations = True
 
-    def create_user(
-        self, email=None, password=None, **extra_fields
-    ):  # pragma: no cover
-        """Create and save a User."""
-        """Create and save a User."""
+    def _create_user(self, username, email, password, **extra_fields): # pragma: no cover
+        """
+        Create and save a user with the given username, email, and password.
+        """
+        if not username:
+            raise ValueError("A username must be supplied.")
+        email = self.normalize_email(email)
         if not email:
             raise ValueError(_("An email address must be supplied."))
         if not password:
             raise ValueError(_("A password must be supplied."))
-        email = self.normalize_email(email)
-        user = self.model(email=email, **extra_fields)
-        user.set_password(password)
-        user.save()
+        # Lookup the real model class from the global app registry so this
+        # manager method can be used in migrations. This is fine because
+        # managers are by definition working on the real model.
+        GlobalUserModel = apps.get_model(
+            self.model._meta.app_label, self.model._meta.object_name
+        )
+        username = GlobalUserModel.normalize_username(username)
+        user = self.model(username=username, email=email, **extra_fields)
+        user.password = make_password(password)
+        user.save(using=self._db)
+
         return user
 
-    def create_superuser(
-        self, email=None, password=None, **extra_fields
-    ):  # pragma: no cover
-        """Create and save a User."""
-        """Create and save a SuperUser."""
+    def create_user(self, username, email=None, password=None, **extra_fields): # pragma: no cover
+        """
+        Create and save a standard user with the supplied username, email, and password.
+        """
+        extra_fields.setdefault("is_active", True)
+        extra_fields.setdefault("is_staff", False)
+        extra_fields.setdefault("is_superuser", False)
+        return self._create_user(username, email, password, **extra_fields)
+
+    def create_superuser(self, username, email=None, password=None, **extra_fields): # pragma: no cover
+        """
+        Create and save a super user with the supplied username, email, and password.
+        """
+        extra_fields.setdefault("is_active", True)
         extra_fields.setdefault("is_staff", True)
         extra_fields.setdefault("is_superuser", True)
-        extra_fields.setdefault("is_active", True)
         extra_fields.setdefault("user_type", "SUPERUSER")
 
         if extra_fields.get("is_staff") is not True:
@@ -44,7 +63,34 @@ def create_superuser(
         if extra_fields.get("user_type") != "SUPERUSER":
             raise ValueError(_("Superuser must be type SUPERUSER."))
 
-        return self.create_user(email, password, **extra_fields)
+        return self._create_user(username, email, password, **extra_fields)
+
+    def with_perm(
+        self, perm, is_active=True, include_superusers=True, backend=None, obj=None
+    ): # pragma: no cover
+        if backend is None:
+            backends = auth._get_backends(return_tuples=True)
+            if len(backends) == 1:
+                backend, _ = backends[0]
+            else:
+                raise ValueError(
+                    "You have multiple authentication backends configured and "
+                    "therefore must provide the `backend` argument."
+                )
+        elif not isinstance(backend, str):
+            raise TypeError(
+                "backend must be a dotted import path string (got %r)." % backend
+            )
+        else:
+            backend = auth.load_backend(backend)
+        if hasattr(backend, "with_perm"):
+            return backend.with_perm(
+                perm,
+                is_active=is_active,
+                include_superusers=include_superusers,
+                obj=obj,
+            )
+        return self.none()
 
 
 class CustomUser(AbstractUser):
@@ -62,27 +108,6 @@ class CustomUserType(models.TextChoices):
         STAFF = "STAFF", _("Staff")
         SUPERUSER = "SUPERUSER", _("Superuser")
 
-    is_staff = models.BooleanField(
-        _("Is Staff"),
-        default=False,
-        help_text="Displays if the user is currently a staff member.",
-    )
-    is_active = models.BooleanField(
-        _("Is Active"),
-        default=True,
-        help_text="Displays if the user is currently an active user.",
-    )
-    date_joined = models.DateTimeField(
-        _("Date Joined"),
-        default=timezone.now,
-        help_text="Displays the user join date.",
-    )
-    email = models.EmailField(
-        _("Email Address"),
-        unique=True,
-        max_length=255,
-        help_text="Displays if the user email address",
-    )
     user_type = models.CharField(
         _("Type"),
         null=False,
@@ -92,6 +117,3 @@ class CustomUserType(models.TextChoices):
         default=CustomUserType.FREE,
         help_text="Displays the users current user type.",
     )
-
-    def __str__(self):
-        return self.username


Test patch:
diff --git a/{{cookiecutter.git_project_name}}/conftest.py b/{{cookiecutter.git_project_name}}/tests/conftest.py
similarity index 100%
rename from {{cookiecutter.git_project_name}}/conftest.py
rename to {{cookiecutter.git_project_name}}/tests/conftest.py