I thank you so much for your help!
A lot of copy and paste with slight adjustments, but working for my case…
A little documentation for someone in the future how I did it:
In my admin.py
I extended my model:
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .admin_forms import AdminPinChangeForm, UserChangeForm, UserCreationForm
class UserAdmin(BaseUserAdmin):
...
form = UserChangeForm
add_form = UserCreationForm
change_pin_form = AdminPinChangeForm
def get_urls(self):
return [
path(
"<id>/pin/",
self.admin_site.admin_view(self.user_change_pin),
name="auth_user_pin_change",
),
] + super().get_urls()
@sensitive_post_parameters_m
def user_change_pin(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.model._meta.verbose_name,
"key": escape(id),
}
)
if request.method == "POST":
form = self.change_pin_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("Pin 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_pin_form(user)
fieldsets = [(None, {"fields": list(form.base_fields)})]
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
context = {
"title": _("Change pin: %s") % escape(user.get_username()),
"adminForm": adminForm,
"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.model._meta,
"original": user,
"save_as": False,
"show_save": True,
**self.admin_site.each_context(request),
}
request.current_app = self.admin_site.name
return TemplateResponse(
request,
get_template('change_pin.html'),
context,
)
Its basically all the password parts from the django.contrib.auth.admin.UserAdmin
copied and edited for pin.
Then I needed a custom user create and user change form in the admin.py
(I created an admin_forms.py
and imported it from there):
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
from django.contrib.auth.forms import UserCreationForm as BaseUserCreateForm
class UserCreationForm(BaseUserCreateForm):
pin = forms.CharField(
label=_("Pin"),
widget=forms.NumberInput(
attrs={"autofocus": True, "min": 0}
),
strip=False,
validators=[MinLengthValidator(4)]
)
def clean_pin(self):
pin = self.cleaned_data.get("pin")
if not pin:
raise ValidationError(
self.error_messages["pin_not_set"],
code="pin_not_set",
)
if not pin.isnumeric():
raise ValidationError(
self.error_messages["pin_not_numeric"],
code="pin_not_numeric"
)
return pin
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_pin(self.cleaned_data["pin"])
if commit:
user.save()
return user
class UserChangeForm(BaseUserChangeForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
disabled password hash display field.
"""
pin = ReadOnlyPasswordHashField(
label=_("Pin"),
help_text=_(
"Raw pins are not stored, so there is no way to see this "
"user’s pin, but you can change the pin using "
'<a href="{}">this form</a>.'
),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
pin = self.fields.get("pin")
if pin:
pin.help_text = pin.help_text.format("../pin/")
user_permissions = self.fields.get("user_permissions")
if user_permissions:
user_permissions.queryset = user_permissions.queryset.select_related(
"content_type"
)
In the ModelAdmin I created that new Link, now I need to pass a Form there, for me it is the following (also created in admin_forms.py
):
class AdminPinChangeForm(forms.Form):
"""
A form used to change the pin of a user in the admin interface.
"""
error_messages = {
"pin_not_set": _("The Pin field must be set."),
"pin_not_numeric": _("The Pin field must be numeric."),
}
required_css_class = "required"
pin = forms.CharField(
label=_("Pin"),
widget=forms.NumberInput(
attrs={"autofocus": True, "min": 0}
),
strip=False,
validators=[MinLengthValidator(4)]
)
def __init__(self, user, *args, **kwargs):
self.user = user
super().__init__(*args, **kwargs)
def clean_pin(self):
pin = self.cleaned_data.get("pin")
if not pin:
raise ValidationError(
self.error_messages["pin_not_set"],
code="pin_not_set",
)
if not pin.isnumeric():
raise ValidationError(
self.error_messages["pin_not_numeric"],
code="pin_not_numeric"
)
return pin
def save(self, commit=True):
"""Save the new pin."""
pin = self.cleaned_data["pin"]
self.user.set_pin(pin)
if commit:
self.user.save()
return self.user
@property
def changed_data(self):
data = super().changed_data
for name in self.fields:
if name not in data:
return []
return ["pin"]
Now I needed a custom template, so I copied the change_password.html
to my change_pin.html
inside my created templates folder.
change_pin.html
:
{% extends "admin/base_site.html" %}
{% load i18n static %}
{% load admin_urls %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">{% endblock %}
{% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %}
{% if not is_popup %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a>
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
› <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
› {% translate 'Change Pin' %}
</div>
{% endblock %}
{% endif %}
{% block content %}<div id="content-main">
<form{% if form_url %} action="{{ form_url }}"{% endif %} method="post" id="{{ opts.model_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
<input type="text" name="username" value="{{ original.get_username }}" class="hidden">
<div>
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
{% if form.errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
<p>{% blocktranslate with username=original %}Enter a new pin for the user <strong>{{ username }}</strong>.{% endblocktranslate %}</p>
<fieldset class="module aligned">
<div class="form-row">
{{ form.pin.errors }}
{{ form.pin.label_tag }} {{ form.pin }}
{% if form.pin.help_text %}
<div class="help">{{ form.pin.help_text|safe }}</div>
{% endif %}
</div>
</fieldset>
<div class="submit-row">
<input type="submit" value="{% translate 'Set pin' %}" class="default">
</div>
</div>
</form></div>
{% endblock %}
What I forgot to mention are the basics - I added a function set_pin()
to my Model MyUserModel
:
class MyUserModel(AbstractBaseUser, PermissionsMixin):
...
objects = MyUserManager()
...
def set_pin(self, raw_pin):
self.pin = make_password(raw_pin)
and I also want to hash the field when adding that user from the terminal, so I edit my ModelManager MyUserManager
:
class MyUserManager(BaseUserManager):
use_in_migrations: bool = True
def _create_user(self, username, password, pin, first_name, is_active, is_staff, is_superuser, **extra_fields):
user = self.model(
username=username,
first_name=first_name,
is_staff=is_staff,
is_active=is_active,
is_superuser=is_superuser,
)
user.set_password(password)
user.set_pin(pin)
user.save(using=self._db)
return user
def create_superuser(self, username, password, pin, first_name):
return self._create_user(username, password, pin, first_name, True, True, True)
def create_user(self, username, password, pin, first_name):
return self._create_user(username, password, pin, first_name, True, False, False)
and thats basically it.
Thank you very much for your help @KenWhitesell ! Im so happy to have it now working the way I wanted it!