From 919c60d9697a1e0828f52cb6522d434713cde210 Mon Sep 17 00:00:00 2001 From: renzo Date: Wed, 17 Jul 2024 22:21:58 -0300 Subject: [PATCH] =?UTF-8?q?Extraido=20lib=20para=20usu=C3=A1rio=20customiz?= =?UTF-8?q?ado=20do=20DJango?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #23 --- backend/devpro/base/admin.py | 211 +----------------- backend/devpro/base/manager.py | 62 ----- .../devpro/base/migrations/0001_initial.py | 6 +- backend/devpro/base/models.py | 67 +----- backend/devpro/base/tests/test_manager.py | 5 +- backend/poetry.lock | 16 +- backend/pyproject.toml | 1 + 7 files changed, 26 insertions(+), 342 deletions(-) delete mode 100644 backend/devpro/base/manager.py diff --git a/backend/devpro/base/admin.py b/backend/devpro/base/admin.py index e42079a..fb9887e 100644 --- a/backend/devpro/base/admin.py +++ b/backend/devpro/base/admin.py @@ -1,212 +1,9 @@ -from django.conf import settings -from django.contrib import admin, messages -from django.contrib.admin.options import IS_POPUP_VAR -from django.contrib.admin.utils import unquote -from django.contrib.auth import update_session_auth_hash -from django.contrib.auth.admin import sensitive_post_parameters_m -from django.contrib.auth.forms import UserChangeForm, UserCreationForm, AdminPasswordChangeForm -from django.core.exceptions import PermissionDenied -from django.db import transaction, router -from django.http import Http404, HttpResponseRedirect -from django.template.response import TemplateResponse -from django.urls import path, reverse -from django.utils.decorators import method_decorator -from django.utils.html import escape -from django.views.decorators.csrf import csrf_protect +from django.contrib import admin +from django_min_custom_user.admin import MinUserAdmin from devpro.base.models import User -from django.utils.translation import gettext_lazy as _, gettext - -csrf_protect_m = method_decorator(csrf_protect) @admin.register(User) -class UserAdmin(admin.ModelAdmin): - add_form_template = "admin/auth/user/add_form.html" - change_user_password_template = None - fieldsets = ( - (None, {"fields": ("email", "password")}), - (_("Personal info"), {"fields": ("first_name",)}), - ( - _("Permissions"), - { - "fields": ( - "is_active", - "is_staff", - "is_superuser", - "groups", - "user_permissions", - ), - }, - ), - (_("Important dates"), {"fields": ("last_login", "date_joined")}), - ) - add_fieldsets = ( - ( - None, - { - "classes": ("wide",), - "fields": ("email", "password1", "password2"), - }, - ), - ) - form = UserChangeForm - add_form = UserCreationForm - change_password_form = AdminPasswordChangeForm - list_display = ("email", "first_name", "is_staff") - list_filter = ("is_staff", "is_superuser", "is_active", "groups") - search_fields = ("first_name", "email") - ordering = ("email",) - filter_horizontal = ( - "groups", - "user_permissions", - ) - - def get_fieldsets(self, request, obj=None): - if not obj: - return self.add_fieldsets - return super().get_fieldsets(request, obj) - - def get_form(self, request, obj=None, **kwargs): - """ - Use special form during user creation - """ - defaults = {} - if obj is None: - defaults["form"] = self.add_form - defaults.update(kwargs) - return super().get_form(request, obj, **defaults) - - def get_urls(self): - return [ - path( - "/password/", - self.admin_site.admin_view(self.user_change_password), - name="auth_user_password_change", - ), - ] + super().get_urls() - - # RemovedInDjango60Warning: when the deprecation ends, replace with: - # def lookup_allowed(self, lookup, value, request): - def lookup_allowed(self, lookup, value, request=None): - # Don't allow lookups involving passwords. - return not lookup.startswith("password") and super().lookup_allowed( - lookup, value, request - ) - - @sensitive_post_parameters_m - @csrf_protect_m - def add_view(self, request, form_url="", extra_context=None): - with transaction.atomic(using=router.db_for_write(self.model)): - return self._add_view(request, form_url, extra_context) - - def _add_view(self, request, form_url="", extra_context=None): - # It's an error for a user to have add permission but NOT change - # permission for users. If we allowed such users to add users, they - # could create superusers, which would mean they would essentially have - # the permission to change users. To avoid the problem entirely, we - # disallow users from adding users if they don't have change - # permission. - if not self.has_change_permission(request): - if self.has_add_permission(request) and settings.DEBUG: - # Raise Http404 in debug mode so that the user gets a helpful - # error message. - raise Http404( - 'Your user does not have the "Change user" permission. In ' - "order to add users, Django requires that your user " - 'account have both the "Add user" and "Change user" ' - "permissions set." - ) - raise PermissionDenied - if extra_context is None: - extra_context = {} - username_field = self.opts.get_field(self.model.USERNAME_FIELD) - defaults = { - "auto_populated_fields": (), - "username_help_text": username_field.help_text, - } - extra_context.update(defaults) - return super().add_view(request, form_url, extra_context) - - @sensitive_post_parameters_m - def user_change_password(self, request, id, form_url=""): - user = self.get_object(request, unquote(id)) - if not self.has_change_permission(request, user): - raise PermissionDenied - if user is None: - raise Http404( - _("%(name)s object with primary key %(key)r does not exist.") - % { - "name": self.opts.verbose_name, - "key": escape(id), - } - ) - if request.method == "POST": - form = self.change_password_form(user, request.POST) - if form.is_valid(): - form.save() - change_message = self.construct_change_message(request, form, None) - self.log_change(request, user, change_message) - msg = gettext("Password changed successfully.") - messages.success(request, msg) - update_session_auth_hash(request, form.user) - return HttpResponseRedirect( - reverse( - "%s:%s_%s_change" - % ( - self.admin_site.name, - user._meta.app_label, - user._meta.model_name, - ), - args=(user.pk,), - ) - ) - else: - form = self.change_password_form(user) - - fieldsets = [(None, {"fields": list(form.base_fields)})] - admin_form = admin.helpers.AdminForm(form, fieldsets, {}) - - context = { - "title": _("Change password: %s") % escape(user.get_username()), - "adminForm": admin_form, - "form_url": form_url, - "form": form, - "is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET), - "is_popup_var": IS_POPUP_VAR, - "add": True, - "change": False, - "has_delete_permission": False, - "has_change_permission": True, - "has_absolute_url": False, - "opts": self.opts, - "original": user, - "save_as": False, - "show_save": True, - **self.admin_site.each_context(request), - } - - request.current_app = self.admin_site.name - - return TemplateResponse( - request, - self.change_user_password_template - or "admin/auth/user/change_password.html", - context, - ) - - def response_add(self, request, obj, post_url_continue=None): - """ - Determine the HttpResponse for the add_view stage. It mostly defers to - its superclass implementation but is customized because the User model - has a slightly different workflow. - """ - # We should allow further modification of the user just added i.e. the - # 'Save' button should behave like the 'Save and continue editing' - # button except in two scenarios: - # * The user has pressed the 'Save and add another' button - # * We are adding a user in a popup - if "_addanother" not in request.POST and IS_POPUP_VAR not in request.POST: - request.POST = request.POST.copy() - request.POST["_continue"] = 1 - return super().response_add(request, obj, post_url_continue) +class UserAdmin(MinUserAdmin): + pass diff --git a/backend/devpro/base/manager.py b/backend/devpro/base/manager.py deleted file mode 100644 index c4eec9b..0000000 --- a/backend/devpro/base/manager.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib import auth -from django.contrib.auth.base_user import BaseUserManager -from django.contrib.auth.hashers import make_password - - -class UserManager(BaseUserManager): - use_in_migrations = True - - def _create_user(self, email, password, **extra_fields): - """ - Create and save a user with the given username, email, and password. - """ - if not email: - raise ValueError("The given email must be set") - email = self.normalize_email(email).lower() - user = self.model(email=email, **extra_fields) - user.password = make_password(password) - user.save(using=self._db) - return user - - def create_user(self, email: str, password=None, **extra_fields): - extra_fields.setdefault("is_staff", False) - extra_fields.setdefault("is_superuser", False) - return self._create_user(email, password, **extra_fields) - - def create_superuser(self, email, password=None, **extra_fields): - 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, **extra_fields) - - def with_perm( - self, perm, is_active=True, include_superusers=True, backend=None, obj=None - ): - 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() diff --git a/backend/devpro/base/migrations/0001_initial.py b/backend/devpro/base/migrations/0001_initial.py index 6cda1aa..a7604ba 100644 --- a/backend/devpro/base/migrations/0001_initial.py +++ b/backend/devpro/base/migrations/0001_initial.py @@ -1,8 +1,6 @@ -# Generated by Django 5.0.7 on 2024-07-11 01:13 - -import devpro.base.manager import django.utils.timezone from django.db import migrations, models +from django_min_custom_user.manager import MinUserManager class Migration(migrations.Migration): @@ -68,7 +66,7 @@ class Migration(migrations.Migration): 'abstract': False, }, managers=[ - ('objects', devpro.base.manager.UserManager()), + ('objects', MinUserManager()), ], ), ] diff --git a/backend/devpro/base/models.py b/backend/devpro/base/models.py index 91ae632..9278cdf 100644 --- a/backend/devpro/base/models.py +++ b/backend/devpro/base/models.py @@ -1,68 +1,5 @@ -from django.contrib.auth.base_user import AbstractBaseUser -from django.contrib.auth.models import PermissionsMixin -from django.core.mail import send_mail -from django.db import models -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django_min_custom_user.models import MinAbstractUser -from devpro.base.manager import UserManager - -class AbstractUser(AbstractBaseUser, PermissionsMixin): - """ - An abstract base class implementing a fully featured User model with - admin-compliant permissions. - - Email and password are required. Other fields are optional. - """ - - first_name = models.CharField(_("first name"), max_length=150, blank=True) - email = models.EmailField(_("email address"), unique=True) - is_staff = models.BooleanField( - _("staff status"), - default=False, - help_text=_("Designates whether the user can log into this admin site."), - ) - is_active = models.BooleanField( - _("active"), - default=True, - help_text=_( - "Designates whether this user should be treated as active. " - "Unselect this instead of deleting accounts." - ), - ) - date_joined = models.DateTimeField(_("date joined"), default=timezone.now) - - objects = UserManager() - - EMAIL_FIELD = "email" - USERNAME_FIELD = "email" - REQUIRED_FIELDS = [] - - class Meta: - verbose_name = _("user") - verbose_name_plural = _("users") - abstract = True - - def clean(self): - super().clean() - self.email = self.__class__.objects.normalize_email(self.email) - - def get_full_name(self): - """ - Return the first_name plus the last_name, with a space in between. - """ - full_name = f"{self.first_name}" - return full_name.strip() - - def get_short_name(self): - """Return the short name for the user.""" - return self.first_name - - def email_user(self, subject, message, from_email=None, **kwargs): - """Send an email to this user.""" - send_mail(subject, message, from_email, [self.email], **kwargs) - - -class User(AbstractUser): +class User(MinAbstractUser): pass diff --git a/backend/devpro/base/tests/test_manager.py b/backend/devpro/base/tests/test_manager.py index 03d3372..d53cc00 100644 --- a/backend/devpro/base/tests/test_manager.py +++ b/backend/devpro/base/tests/test_manager.py @@ -1,12 +1,11 @@ import pytest from django.contrib.auth import get_user_model - -from devpro.base.manager import UserManager +from django_min_custom_user.manager import MinUserManager def test_user_has_customized_manager_instance(): User = get_user_model() - assert isinstance(User.objects, UserManager) + assert isinstance(User.objects, MinUserManager) @pytest.mark.django_db diff --git a/backend/poetry.lock b/backend/poetry.lock index 6acfc28..bb9e021 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -60,6 +60,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-min-custom-user" +version = "0.2.0" +description = "A project with a mininum fields Django Custom User" +optional = false +python-versions = "<4.0,>=3.12" +files = [ + {file = "django_min_custom_user-0.2.0-py3-none-any.whl", hash = "sha256:e32baf1c94d4f59e384346504570ae7922a4cd72ade862e44c254cafe794ee3b"}, + {file = "django_min_custom_user-0.2.0.tar.gz", hash = "sha256:185c74a38fe46749b3d713f2e67c6e334273e8c3957b531f24e74f7aa40d750a"}, +] + +[package.dependencies] +django = ">=5.0.7,<6.0.0" + [[package]] name = "flake8" version = "7.1.0" @@ -257,4 +271,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5dbcbba5f17b41c7a71f9faa7221bc63bfa4b4ba2f4e3b5ccccaff381d6ef958" +content-hash = "58a6daed039bbfb30da480394f6829b35bb864bbf45f0ce2ee8ded4907d187f7" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 09a8a9e..ec5fa3b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,6 +12,7 @@ django = ">4" psycopg2 = "^2.9.9" dj-database-url = "^2.2.0" python-decouple = "^3.8" +django-min-custom-user = "^0.2.0" [tool.poetry.group.dev]