0

I am trying to implement registration with email and phone on a website. A user can register with either phone or email or both. If a user keeps both phone and email field empty, a ValidationError is raised, "You cannot leave both phone and email fields blank. You must fill at least one of the fields."

We have separate clean methods for username, email, phone, password. I do not want to implement the above-mentioned validation on save(). I don't want to define a clean method in the User model, either. I have written tests for this form, and they pass. But what errors could possibly arise if I use both clean and clean_fieldname together? Could it become a problem when working with views?

I have 3 questions:

  1. Can I use both clean_fieldname and clean methods in a form?
  2. In what other way can I make sure that user registers with at least phone or email?
  3. How do clean() and validate() works? I have read django documentation, but I don't understand it completely.

Here's the code I implemented.

class RegisterForm(SanitizeFieldsForm, forms.ModelForm):
    email = forms.EmailField(required=False)

    message = _("Phone must have format: +9999999999. Upto 15 digits allowed."
                " Do not include hyphen or blank spaces in between, at the"
                " beginning or at the end.")
    phone = forms.RegexField(regex=r'^\+(?:[0-9]?){6,14}[0-9]$',
                             error_messages={'invalid': message},
                             required=False)
    password = forms.CharField(widget=forms.PasswordInput())
    MIN_LENGTH = 10

    class Meta:
        model = User
        fields = ['username', 'email', 'phone', 'password',
                  'full_name']

    class Media:
        js = ('js/sanitize.js', )

    def clean(self):
        super(RegisterForm, self).clean()

        email = self.data.get('email')
        phone = self.data.get('phone')

        if (not phone) and (not email):
            raise forms.ValidationError(
                _("You cannot leave both phone and email empty."
                  " Signup with either phone or email or both."))

    def clean_username(self):
        username = self.data.get('username')
        check_username_case_insensitive(username)
        if username.lower() in settings.CADASTA_INVALID_ENTITY_NAMES:
            raise forms.ValidationError(
                _("Username cannot be “add” or “new”."))
        return username

    def clean_password(self):
        password = self.data.get('password')
        validate_password(password)
        errors = []

        email = self.data.get('email')
        if email:
            email = email.split('@')
            if email[0].casefold() in password.casefold():
                errors.append(_("Passwords cannot contain your email."))

        username = self.data.get('username')
        if len(username) and username.casefold() in password.casefold():
            errors.append(
                _("The password is too similar to the username."))

        phone = self.data.get('phone')
        if phone:
            if phone_validator(phone):
                phone = str(parse_phone(phone).national_number)
                if phone in password:
                    errors.append(_("Passwords cannot contain your phone."))

        if errors:
            raise forms.ValidationError(errors)

        return password

    def clean_email(self):
        email = self.data.get('email')
        if email:
            if User.objects.filter(email=email).exists():
                raise forms.ValidationError(
                    _("Another user with this email already exists"))
        return email

    def clean_phone(self):
        phone = self.data.get('phone')
        if phone:
            if User.objects.filter(phone=phone).exists():
                raise forms.ValidationError(
                    _("Another user with this phone already exists"))
        return phone

    def save(self, *args, **kwargs):
        user = super().save(*args, **kwargs)
        user.set_password(self.cleaned_data['password'])
        user.save()
        return user
Sardorbek Imomaliev
  • 14,861
  • 2
  • 51
  • 63
Parthvi Vala
  • 73
  • 2
  • 6

1 Answers1

1

You can get a lot out of reading the Django code; it is a well-commented codebase! The relevant section is in django/forms/forms.py. When a form is cleaned/validated, it will call full_clean. This will first call _clean_fields, which calls the field clean and looks for a clean_{fieldname} method on the form to call. Then, the form clean is called.

def full_clean(self):
    """
    Clean all of self.data and populate self._errors and self.cleaned_data.
    """
    self._errors = ErrorDict()
    if not self.is_bound:  # Stop further processing.
        return
    self.cleaned_data = {}
    # If the form is permitted to be empty, and none of the form data has
    # changed from the initial data, short circuit any validation.
    if self.empty_permitted and not self.has_changed():
        return

    self._clean_fields()
    self._clean_form()
    self._post_clean()

def _clean_fields(self):
    for name, field in self.fields.items():
        # value_from_datadict() gets the data from the data dictionaries.
        # Each widget type knows how to retrieve its own data, because some
        # widgets split data over several HTML fields.
        if field.disabled:
            value = self.get_initial_for_field(field, name)
        else:
            value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
        try:
            if isinstance(field, FileField):
                initial = self.get_initial_for_field(field, name)
                value = field.clean(value, initial)
            else:
                value = field.clean(value)
            self.cleaned_data[name] = value
            if hasattr(self, 'clean_%s' % name):
                value = getattr(self, 'clean_%s' % name)()
                self.cleaned_data[name] = value
        except ValidationError as e:
            self.add_error(name, e)

def _clean_form(self):
    try:
        cleaned_data = self.clean()
    except ValidationError as e:
        self.add_error(None, e)
    else:
        if cleaned_data is not None:
            self.cleaned_data = cleaned_data
wmorrell
  • 4,988
  • 4
  • 27
  • 37
  • 1
    Actually, the [validators documentation](https://docs.djangoproject.com/en/1.11/ref/forms/validation/) explains this order explicitly too. – Daniel Roseman Jul 03 '17 at 10:02