Search code examples
djangodjango-modelsdjango-viewsdjango-forms

Why updating object models in Django doesn't work as expected?


This seems strange

I have the following model in Django:

class CustomUser(AbstractUser):
    first_name = models.CharField(_("first name"), max_length=20)
    last_name = models.CharField(_("last name"), max_length=20)
    email = models.EmailField(_('email address'), blank=False, unique=True)
    validated_account = models.BooleanField(_('validated account'), blank=False, default=False)
    phone = PhoneNumberField(null=False, blank=False, unique=True)

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

I have the following form to edit the model:

class CustomUserChangeForm(UserChangeForm):
    password = None
    
    class Meta(UserChangeForm.Meta):
        model = CustomUser
        fields = ["first_name", "last_name", "email", "phone", "username"]

This is my view to edit the model:

@user_passes_test(lambda u: u.is_authenticated, "users:login")
def edit_profile(request):
    if request.method == "POST":
        print(request.POST)
        form = CustomUserChangeForm(request.POST, instance=request.user)
        print(form)

        if form.is_valid():
            user = form.save()
            auth_login(request, user)

            messages.success(request, "Edition successful.")

            return redirect("dashboard:cases")

        messages.error(
            request, "Unsuccessful Edition. Invalid information.")
    else:
        form = CustomUserChangeForm(instance=request.user)
        

    return render(request=request, template_name="dashboard/edit_profile.html", context={"edit_profile_form": form})

This is the printing result from the view call:

<QueryDict: {'csrfmiddlewaretoken': ['DbmuCTIOAXdP96Ozrcsf3YXSCxQ1lusywgLz4OLh5FAHrjas25inxqIt0kS9syaj'], 'first_name': ['Super'], 'last_name': ['User'], 'email': ['admin@example.com'], 'phone': ['+0043328107a'], 'username': ['admin']}>
<tr>
    <th><label for="id_first_name">First name:</label></th>
    <td>
      <input type="text" name="first_name" value="Super" maxlength="20" required id="id_first_name">
    </td>
  </tr>
  <tr>
    <th><label for="id_last_name">Last name:</label></th>
    <td>
      <input type="text" name="last_name" value="User" maxlength="20" required id="id_last_name">
    </td>
  </tr>
  <tr>
    <th><label for="id_email">Email address:</label></th>
    <td>
      <input type="email" name="email" value="admin@example.com" maxlength="254" required id="id_email">
    </td>
  </tr>
  <tr>
    <th><label for="id_phone">Phone:</label></th>
    <td>
      <input type="tel" name="phone" value="+0043328107" maxlength="128" required id="id_phone">
    </td>
  </tr>
  <tr>
    <th><label for="id_username">Username:</label></th>
    <td>
      <input type="text" name="username" value="admin" maxlength="150" autocapitalize="none" autocomplete="username" required id="id_username">
        <br>
        <span class="helptext">Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.</span>
    </td>
  </tr>

Why doesn't the resulting form contain the last letter in the phone number? In the POST request, a phone number with a letter at the end clearly appears. I would expect it to be not valid, and instead the form is valid.

What am I doing wrong? Thanks for the help


Solution

  • I'm assuming the PhoneNumberField is from django-phonenumber-field. If so,

    Short answer: That's the library doing it.

    Long answer:
    The specified behaviour of html input of type tel is to allow almost all characters since phone numbers have varied syntax.

    Unlike the URL and Email types, the Telephone type does not enforce a particular syntax. This is intentional; in practice, telephone number fields tend to be free-form fields, because there are a wide variety of valid phone numbers.

    Except for a carriage return /r or line feed /n, the input will let you type in anything, including a mixture of letters and numbers as is your case. That's why the POSTed data contains the 'invalid' character.

    But you're using PhoneNumberField for that field, that's what's responsible for the 'unexpected' behaviour. Django forms implement to_python method which is responsible for converting the user html inputs into Python objects. The returned value is what's used in the form/model's clean method. And this is where the 'unexpected' behaviour originates.

    PhoneNumberField overrides this to_python method, and when supplied a string (which is always the case with html forms), it parses the input using phonenumbers's parse function. This is what's removing the invalid character in your bound form field's value.

    Here is a demonstration phonenumbers' parse method

    You can see the national_number attribute has stripped off the trailing invalid characters. It does the parsing for invalid characters in between the phone number as well. The exact implementation of how it does this is explained in the parse function's doc string:

    """The method is quite lenient and looks for a number in the input text (raw input) and does not check whether the string is definitely only a phone number. To do this, it ignores punctuation and white-space, as well as any text before the number (e.g. a leading "Tel: ") and trims the non-number bits. It will accept a number in any format (E164, national, international etc), assuming it can be interpreted with the defaultRegion supplied. It also attempts to convert any alpha characters into digits if it thinks this is a vanity number of the type "1800 MICROSOFT".

    Again, this assumes you're using django-phonenumber-field, if not, well, I am dammed.