1

My forms are not showing the error messages when I try to submit an empty form but I can see the errors while looping over the field errors in views.py. How do I overcome this problem?

the template (Updated):

    {% block formcontent %}
    {{form.non_field_errors}}
    <div class="row">
        <div class="col">
            {{form.username.label_tag}} {{form.username}} {{form.username.errors|striptags}}
        </div>
    </div><br>
    <div class="row">
        <div class="col">
            {{form.first_name.label_tag}} {{form.first_name}} {{form.first_name.errors|striptags}}
        </div>
        <div class="col">
            {{form.last_name.label_tag}} {{form.last_name}} {{form.last_name.errors|striptags}}
        </div>
    </div><br>
    <div class="row">
        <div class="col">
            {{form.email.label_tag}} {{form.email}} {{form.email.errors|striptags}}
        </div>
    </div><br>
    <div class="row">
        <div class="col">
            {{form.location.label_tag}} {{form.location}} {{form.location.errors|striptags}}
        </div>
        <div class="col">
            {{form.designation.label_tag}} {{form.designation}} {{form.designation.errors|striptags}}
        </div>
    </div><br>
    <div class="row">
        <div class="col">
            {{form.password1.label_tag}} {{form.password1}} {{form.password1.errors|striptags}}
        </div>
        <div class="col">
            {{form.password2.label_tag}} {{form.password2}} {{form.password2.errors|striptags}}
        </div>
    </div><br>
{% endblock formcontent %}

Edit 1: (Updated)

class MyRegistrationForm(UserCreationForm):
password1=forms.CharField(label='Password', widget=forms.PasswordInput(attrs={'class':'form-control'}))
password2=forms.CharField(label='Confirm Password', widget=forms.PasswordInput(attrs={'class':'form-control'}))
class Meta:
    model=MyRegistration
    fields=['username', 'first_name', 'last_name', 'email', 'location', 'designation']
    widgets={
        'username':forms.TextInput(attrs={'class':'form-control'}),
        'first_name':forms.TextInput(attrs={'class':'form-control'}),
        'last_name':forms.TextInput(attrs={'class':'form-control'}),
        'email':forms.EmailInput(attrs={'class':'form-control'}),
        'location':forms.Select(attrs={'class':'form-select'}),
        'designation':forms.TextInput(attrs={'class':'form-control'}),
    }

def clean_username(self):
    username = self.cleaned_data.get('username')
    if not username:
        raise ValidationError('Username is required!')
    else:
        try:
            MyRegistration.objects.get(username=username)
            raise ValidationError('This username already exists!', code='username_exists')
        except MyRegistration.DoesNotExist:
            pass
    return username

def clean_email(self):
    email=self.cleaned_data.get('email')
    if not email:
        raise ValidationError('Email is required!')
    else:
        try:
            MyRegistration.objects.get(email=email)
            raise ValidationError('This email already exists!', code='email_exists')
        except MyRegistration.DoesNotExist:
            pass
    return email

def clean_first_name(self):
    first_name=self.cleaned_data.get('first_name')
    if not first_name:
        raise ValidationError('First-name is required!')
    return first_name

def clean_last_name(self):
    last_name=self.cleaned_data.get('last_name')
    if not last_name:
        raise ValidationError('Last-name is required!')
    return last_name

def clean_location(self):
    location=self.cleaned_data.get('location')
    if not location:
        raise ValidationError('Location is required!')
    return location

def clean_designation(self):
    designation=self.cleaned_data.get('designation')
    if not designation:
        raise ValidationError('Designation is required!')
    return designation

I really have no idea what is wrong with my codes in template. I have checked, the Django documentation suggests the same way to approach such scenarios where the forms are not looped over.

Edit 2:

models.py:

class MyRegistration(AbstractBaseUser, PermissionsMixin):
    location_list=[
        ('Solapur', 'Solapur'),
        ('Dhule', 'Dhule'),
        ('Other', 'Other'),
        ]
    username=models.CharField(max_length=10, unique=True)
    email=models.EmailField(unique=True)
    first_name=models.CharField(max_length=150)
    last_name=models.CharField(max_length=150)
    location=models.CharField(max_length=10, choices=location_list, default=None)
    designation=models.CharField(max_length=70)
    is_active=models.BooleanField()
    is_staff=models.BooleanField(default=False)
    start_date=models.DateTimeField(default=timezone.now)
    last_login=models.DateTimeField(null=True)


    USERNAME_FIELD='username'
    REQUIRED_FIELDS=['email', 'first_name', 'last_name', 'location', 'designation']
    objects=FirstManager()
    def __str__(self):
        return self.first_name

views.py:(Updated)

def signup(request):
print('1')
if request.user.is_authenticated:
    print('2')
    if request.method=='POST':
        print('3')
        if request.POST.get('password1')==request.POST.get('password2'):
            print('4')
            fm=MyRegistrationForm(request.POST)
            for field in fm:
                print("Field Error:", field.name,  field.errors)
            if fm.is_valid():
                print('6')
                fm.save()
                messages.success(request, 'Registered successfully!!')
            fm=MyRegistrationForm()
            print('7')
            cur_user=request.user
            return render(request, 'account/signup.html', {'form':fm, 'cur_user':cur_user})
    else:
        fm=MyRegistrationForm()
        cur_user=request.user
        return render(request, 'account/signup.html', {'form':fm, 'cur_user':cur_user})
else:
    return HttpResponseRedirect('/')
noob87
  • 41
  • 10

1 Answers1

1

When you raise ValidationError in the clean method, these errors get added to the non_field_errors attribute on the form. This is why nothing gets rendered when using form.email.errors and other errors attributes on particular fields.

You should render the form.non_field_errors before you render your form, so you can see those errors, too.

However, to solve your issue, I would rather go with the option of splitting the validation of each field into particular methods clean_<field_name>. For example for username field:

def clean_username(self):
    username = self.cleaned_data.get('username')
    if not username:
        raise ValidationError('Username is required!')
    else:
        try:
            un=MyRegistration.objects.get(username=self.instance.username)
            raise ValidationError('This username already exists!')
        except MyRegistration.DoesNotExist:
            pass

    # Make sure you return the value of the data in 
    # the clean_<field_name> methods
    return username

And so on for other fields, too. Doing just this should fix your code, but here are some other recommendations:

  • Use codes when raising ValidationErrors. E.g.: raise ValidationError('This username already exists', code='username_exists')
  • Check out the django-crispy package, it can handle the HTML rendering of forms with minimal code
  • You can set constraints in the models for unique fields (e.g. username) and required fields. This further prevents users who are not adding data with your form (e.g. admin) to add duplicate usernames or null values. This would also mean that a lot of your custom validation code would be unnecessary.

EDIT #1: The use of instance to get the values from the submitted form is wrong. Since this form is used exclusively for create purposes as registration is creation of a new user, instance will always be empty. instance is only filled with data when you're updating a model instance.

You should replace the uses of instance with getting the form data from self.cleaned_data dict. For example:

# Instead of:
username = self.instance.username
# Use:
username = self.cleaned_data.get('username')

EDIT #2: After the author added the view code.

The issue might be in your view code. Also, there is no need to comparing password1 and password2 as the UserCreationForm already does that for your.

The core issue is that if your form is invalid, you need to re-render that same form, not create another instance. I propose the following update:

def signup(request):
    print('1')
    if request.user.is_authenticated:
        print('2')
        if request.method=='POST':
            print('3')
            form = MyRegistrationForm(request.POST)
            if form.is_valid():
                print('4')
                form.save()
                messages.success(request, 'Registered successfully!!')
            # If you do this, you always render the empty form
            # without the errors
            # fm=MyRegistrationForm()
            print('7')
            cur_user=request.user
            return render(
                request, 'account/signup.html', 
                {'form': form, 'cur_user':cur_user}
            )
        else:
            form = MyRegistrationForm()
            cur_user=request.user
            return render(
                request, 'account/signup.html', 
                {'form':form, 'cur_user':cur_user}
            )
    else:
        return HttpResponseRedirect('/')

Some other recommendations:

  • You probably don't need to check if the user is authenticated if this is registration view. How can the new users create an account? However, if this is needed probably the @login_required decorator works better for this.
  • On success, you need to redirect to the success URL. Don't use render for success scenarios, only when handling the GET method or when you need to re-render the form to display validation errors.
vinkomlacic
  • 1,822
  • 1
  • 9
  • 19
  • It didn't help actually. I tried rendering the `form.non_field_errors` , didn't work. Created separate `clean` methods for each fields too, no effect! – noob87 Jan 15 '23 at 01:56
  • Can you add what you see in the errors? – vinkomlacic Jan 15 '23 at 09:26
  • I tried testing this, and it works for me (`non_field_errors` get rendered). So, I suspect the errors might be in views or model code. – vinkomlacic Jan 15 '23 at 11:00
  • 1
    I also added an edit regarding the use of `self.instance` – vinkomlacic Jan 15 '23 at 11:06
  • That is the problem, there's no error showing. The form just stays intact when I'm submitting it totally blank, no errors, nothing. So it's become impossible to track down the core issue here. – noob87 Jan 15 '23 at 18:42
  • I have also updated the `MyRegistrationForm` in my post where I did use the cleaned_data approach but to no avail. Hope you find something in the model or the view function there. – noob87 Jan 15 '23 at 18:47
  • I updated the answer to add a correction for the view method – vinkomlacic Jan 15 '23 at 20:15
  • I have updated the view function in my post. Please let me know if this was what you recommended and, if yes, then sadly, that didn't work out too! And about the authentication of the user, actually, The new users can only be created by the superuser. – noob87 Jan 16 '23 at 02:35
  • Another question, will the password matching feature work automatically when I have created my user model by inheriting `AbstractBaseUser` and I'm not using the `UserCreationForm`? – noob87 Jan 16 '23 at 02:39
  • I would also like to mention that none of the forms in my apps display the errors. I have posted this particular form as an example. If this gets fixed, probably others will too. – noob87 Jan 16 '23 at 02:47
  • The password matching feature depends on the fact that your fields are named `password1` and `password2`. If this is true, it will work. See the [docs](https://docs.djangoproject.com/en/4.1/topics/auth/customizing/#a-full-example) and this [SO post](https://stackoverflow.com/questions/27049916/when-to-use-abstractbaseuser-in-django) – vinkomlacic Jan 16 '23 at 08:14
  • PS, I don't see any updates in the original post. Is the current code in the post what you tried? – vinkomlacic Jan 16 '23 at 08:15
  • 1
    You still have the problematic line `fm=MyRegistrationForm()` there. Please try the code as suggested in the answer. – vinkomlacic Jan 16 '23 at 10:28
  • 1
    I really misread that line. It WORKS now!! Thanks a ton! – noob87 Jan 16 '23 at 11:07