Can I apply CSS to UserCreationForm?

I want to apply CSS style in the form class inherited UserCreationForm.
I tried to set attribute in widgets under class Meta, but only password1 and password2 field didn’t work.
So I use the code below under def __init__, and it worked.

self.fields["password1"].widget.attrs["class"] = "form-control"
self.fields["password2"].widget.attrs["class"] = "form-control"

I think it’s because there is no password1 and password2 field in models.py
My question is these.

  1. Is my thought right? (Style didn’t applied because there are no password1 and password2 in models.py)
  2. Is that the only way to apply style on those fields?

For your information, here are my models.py and forms.py
models.py

from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, Group
from django.core.validators import RegexValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone

from branches.models import Branch

# Create your models here.
phone_validator = RegexValidator(
    regex="\d{2,4}-?\d{3,4}(-?\d{4})?",
    message="This is not correct phone number format",
)


class UserManager(BaseUserManager):
    def create_user(
        self,
        username,
        full_name,
        birthday,
        gender,
        phone,
        branch,
        password=None,
        **kwargs,
    ):
        user = self.model(
            username=username,
            full_name=full_name,
            group=Group.objects.get_or_create(name="Members"),
            birthday=birthday,
            gender=gender,
            phone=phone,
            license_type=kwargs["license_type"],
            plan_type=kwargs["plan_type"],
            branch=Branch.objects.get(srl=branch),
        )

        user.set_password(password)
        user.save(using=self._db)

        return user

    def create_staff(
        self, username, full_name, birthday, gender, phone, branch, password, **kwargs
    ):
        user = self.create_user(
            username, full_name, birthday, gender, phone, branch, password, kwargs
        )
        user.staff = True
        user.group = (Group.objects.get_or_create(name="Staffs"),)
        user.save(using=self._db)

        return user

    def create_superuser(
        self, username, full_name, birthday, gender, phone, branch, password, **kwargs
    ):
        user = self.create_user(
            username, full_name, birthday, gender, phone, branch, password, kwargs
        )
        user.staff = True
        user.superuser = True
        user.group = (Group.objects.get_or_create(name="Superusers"),)
        user.save(using=self._db)

        return user


class User(AbstractBaseUser):
    objects = UserManager()

    GENDERS = (
        (None, "Not Chosen"),
        ("M", "Male"),
        ("F", "Female"),
    )
    LICENSE_TYPES = (
        (None, "Not Chosen"),
        ("1L", "Class 1 Large"),
        ("1O", "Class 1 Ordinary"),
        ("1OA", "Class 1 Ordinary (Automatic)"),
        ("2O", "Class 2 Ordinary"),
        ("2OA", "Class 2 Ordinary (Automatic)"),
        ("P", "Practice"),
    )
    PLAN_TYPES = (
        (None, "Not Chosen"),
        ("T", "Time-based"),
        ("G", "Guarantee"),
        ("P", "Practice"),
    )
    srl = models.BigAutoField(
        primary_key=True,
        verbose_name="Serial",
    )
    username = models.TextField(
        unique=True,
        verbose_name="Username",
    )
    full_name = models.TextField(
        verbose_name="Full name",
    )
    password = models.TextField(
        verbose_name="Password",
    )
    groups = models.ManyToManyField(
        to=Group,
        verbose_name="Group",
    )
    birthday = models.DateField(
        verbose_name="Birthday",
    )
    gender = models.TextField(
        verbose_name="Gender",
        choices=GENDERS,
    )
    phone = models.TextField(
        verbose_name="Phone number",
        validators=(phone_validator,),
    )
    branch = models.ForeignKey(
        "branches.Branch",
        verbose_name="Branch",
        on_delete=models.DO_NOTHING,
    )
    license_type = models.CharField(
        max_length=3,
        verbose_name="License type",
        choices=LICENSE_TYPES,
        blank=True,
        null=True,
        default=None,
    )
    plan_type = models.CharField(
        max_length=1,
        verbose_name="Plan type",
        choices=PLAN_TYPES,
        blank=True,
        null=True,
        default=None,
    )
    staff = models.BooleanField(
        verbose_name="Staff",
        default=False,
    )
    active = models.BooleanField(
        verbose_name="Active",
        default=True,
    )
    superuser = models.BooleanField(
        verbose_name="Superuser",
        default=False,
    )
    last_login = models.DateTimeField(
        verbose_name="Last login",
        default=timezone.now,
    )
    date_joined = models.DateTimeField(
        verbose_name="Date joined",
        default=timezone.now,
    )

    USERNAME_FIELD = "username"

    REQUIRED_FIELDS = (
        "name",
        "birthday",
        "gender",
        "phone",
        "branch",
    )

    @property
    def is_staff(self):
        return self.staff

    @property
    def is_active(self):
        return self.active

    @property
    def is_superuser(self):
        return self.superuser

    def has_perm(self, perm, obj=None):
        return self.staff

    def has_module_perms(self, app_label):
        return self.staff

    class Meta:
        verbose_name = "User"
        verbose_name_plural = "Users"
        ordering = [
            "branch",
            "srl",
        ]

    def __str__(self):
        if self.gender == "M":
            gender_short = "M"
        elif self.gender == "F":
            gender_short = "F"

        return f"{self.full_name} ({self.branch}/{self.birthday.strftime('%y%m%d')}/{gender_short})"

    def get_absolute_url(self):
        return reverse("users:detail", kwargs={"srl": self.srl})

forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from django.forms import ModelChoiceField

from branches.models import Branch
from users.models import User


class UserForm(UserCreationForm):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop("user")
        super(UserForm, self).__init__(*args, **kwargs)
        # These work well
        self.fields["password1"].widget.attrs["class"] = "form-control"
        self.fields["password2"].widget.attrs["class"] = "form-control"
        if self.user.is_superuser:
            self.fields["branch"] = ModelChoiceField(
                queryset=Branch.objects.all(),
                required=True,
                label="Branch",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    },
                ),
            )
            self.fields["groups"] = ModelChoiceField(
                queryset=Group.objects.all(),
                required=True,
                label="Group",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    },
                ),
            )
        else:
            self.fields["branch"] = ModelChoiceField(
                queryset=Branch.objects.filter(branch=self.user.branch),
                required=True,
                label="Branch",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    }
                ),
            )
            self.fields["groups"] = ModelChoiceField(
                queryset=Group.objects.filter(name__in=["Member", "Staff"]),
                required=True,
                label="Group",
                widget=forms.Select(
                    attrs={
                        "class": "form-select",
                    },
                ),
            )

    def clean_phone(self):
        phone_number = self.cleaned_data["phone"]
        phone_number = phone_number.replace("-", "")

        if len(phone_number) == 0:
            return phone_number
        elif phone_number[0:2] == "02" and len(phone_number) == 9:
            phone_number = [phone_number[0:2], phone_number[2:5], phone_number[5:]]
            phone_number = "-".join(phone_number)
        elif phone_number[0:2] == "02" and len(phone_number) == 10:
            phone_number = [phone_number[0:2], phone_number[2:6], phone_number[6:]]
            phone_number = "-".join(phone_number)
        elif phone_number[0] != 0 and len(phone_number) == 8:
            phone_number = [phone_number[0:4], phone_number[4:]]
            phone_number = "-".join(phone_number)
        elif len(phone_number) == 10:
            phone_number = [phone_number[0:3], phone_number[3:6], phone_number[6:]]
            phone_number = "-".join(phone_number)
        elif len(phone_number) == 11:
            phone_number = [phone_number[0:3], phone_number[3:7], phone_number[7:]]
            phone_number = "-".join(phone_number)

        return phone_number

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError("Passwords are not matched.")
        return password2

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user

    class Meta:
        model = User
        fields = (
            "username",
            "full_name",
            "password1",
            "password2",
            "groups",
            "birthday",
            "gender",
            "phone",
            "branch",
            "license_type",
            "plan_type",
        )
        widgets = {
            "username": forms.TextInput(
                attrs={
                    "class": "form-control",
                }
            ),
            "full_name": forms.TextInput(
                attrs={
                    "class": "form-control",
                }
            ),
            # These don't work
            # "password1": forms.PasswordInput(
            #     attrs={
            #         "class": "form-control",
            #     }
            # ),
            # "password2": forms.PasswordInput(
            #     attrs={
            #         "class": "form-control",
            #     }
            # ),
            "birthday": forms.DateInput(
                attrs={
                    "type": "date",
                    "class": "form-control",
                }
            ),
            "gender": forms.RadioSelect(
                attrs={
                    "class": "form-check-input",
                },
            ),
            "phone": forms.TextInput(
                attrs={
                    "type": "tel",
                    "class": "form-control",
                },
            ),
            "license_type": forms.Select(
                attrs={
                    "class": "form-select",
                },
            ),
            "plan_type": forms.Select(
                attrs={
                    "class": "form-select",
                },
            ),
            "staff": forms.RadioSelect(
                choices=[(True, "Yes"), (False, "No")],
                attrs={
                    "class": "form-check-input",
                },
            ),
        }

Not quite. It’s not so much because those fields aren’t in the model, but because they’re not defined as fields in the Meta class. (In other words, if those fields were defined in the model, but not referenced by the fields definition, it still wouldn’t have applied.)

No it isn’t.

A ModelForm is a Python class.

A ModelForm is also primarily a Form. It is a Form, where Django can automatically create some fields for you. That information about the auto-created fields is specified in the Meta class.

But just like any other form, you can define fields outside Meta. Since this is a Form, those fields act like any other field in any other form. And since this is a Python class, you can override any definition from a parent class.

example:

class UserForm(UserCreationForm):
   password1 = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={"autocomplete": "new-password", "class": "form-control"}),
        help_text=password_validation.password_validators_help_text_html(),
    )
    ...

Note: This is not a suggestion that you should do it this way - or even that you might want to do it this way. This is solely a demonstration to answer your question about where that type of change can be made, written to illustrate form behavior.

I’m a bit confused now.
Is it mean declaring styles of password1 and password2 in Meta class doesn’t make styles applied is normal?

It’s what you would expect to have happen, yes.

Got it.
Thanks.
That was what I really wanted to know.