1

I have this model:

class IeltsExam(Model):

    student = OneToOneField(Student, on_delete=CASCADE)
    has_taken_exam = BooleanField(default=False,)
    listening = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    reading = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    exam_date = DateField(null=True, blank=True, )

    non_empty_fields = \
        {
            'listening': 'please enter your listening score',
            'reading': 'please enter your reading score',
            'exam_date': 'please specify your exam date',
        }

    def clean(self):
        errors = {}
        if self.has_taken_exam:
            for field_name, field_error in self.non_empty_fields.items():
                if getattr(self, field_name) is None:
                    errors[field_name] = field_error
        if errors:
            raise ValidationError(errors)

and have this modelform

class IeltsExamForm(ModelForm):

    class Meta:
        model = IeltsExam
        fields = ('has_taken_exam', 'listening', 'reading', )

when I submit this form in template, I get the below error:

ValueError at /
'ExamForm' has no field named 'exam_date'.

and

During handling of the above exception ({'listening': ['please enter your listening score'], 'reading': ['please enter your reading score'], 'exam_date': ['please specify your exam date']}), another exception occurred:

The error happens in my view where I am validating the form. My database logic is such that I need to have an exam_date field and it should be mandatory to fill if has_taken_exam is checked. However, in ExamForm, for business reasons, I do not need the exam_date. How can I tell ExamForm to turn a blind eye to the exam_date, as I am not saving the modelform instance?

Amin Ba
  • 1,603
  • 1
  • 13
  • 38

2 Answers2

0

Perform the validation on model's save()

Consider the following model:

class Exam(Model):

    student = OneToOneField(Student, on_delete=CASCADE)
    has_taken_exam = BooleanField(default=False)
    score = FloatField(choices=SCORE_CHOICES, null=True, blank=True)
    exam_date = DateField(null=True, blank=True)

    def save(self, *a, **kw):
        if self.has_taken_exam and not self.exam_date:
            raise ValidationError("Exam date must be set when has_taken_exam is True")
        return super().save()
Nader Alexan
  • 2,127
  • 22
  • 36
  • I need field-specific validation error. I edited the question to reflect this fact. – Amin Ba Oct 06 '19 at 09:04
  • `IeltsExamForm` will call `clean` in your `IeltsExam` model regardless of what is in `exclude`. – Nader Alexan Oct 06 '19 at 09:12
  • I need to tell IeltsExamForm not to take action on the validation error of model exam_date field. How can I do this? – Amin Ba Oct 06 '19 at 09:14
  • 1
    Well, you can override `ModelForm._post_clean()`, check raised errors on `self.instance.full_clean(exclude=exclude, validate_unique=False)` and not `self._update_errors(e)` if the error is produced by a field in `exclude`, but seems like an overkill compared to just restructuring your code to have multiple forms performing different validations instead of validating on the model – Nader Alexan Oct 06 '19 at 09:18
  • well, honestly, I did not understand what I have to do – Amin Ba Oct 06 '19 at 09:23
  • 2
    @NaderAlexan it's never a good idea to raise a `ValidationError` in a model's `save()` method. `ValidationError`s are for `clean()`, because they get collected and returned in the form's `errors` dictionary during the call to `form.is_valid()`. Inside `save()` they just are treated as any other exception and will cause a 500 error as response. – dirkgroten Oct 06 '19 at 12:44
0

After the ModelForm is initialised, it has an instance attribute which is the model instance on which clean() will be called. So if you remove exam_date from the instance's non_empty_fields dictionary, it won't use it in clean:

class IeltsExamForm(ModelForm): 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.instance.non_empty_fields.pop('exam_date')

And you could do that for each field in self._meta.exclude.

However, when doing that, the attribute non_empty_fields should not be a class attribute but an instance property. Modifying the instance's non_empty_fields actually modifies the class attribute (it's a dictionary so it's mutable), which will have unintended side-effects (once removed, it's removed for any subsequent instance you create). Change your model to set the attribute in the init method:

class IeltsExam(Model):
    # ...
    # remove the class attribute non_empty_fields

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.non_empty_fields = { ... }

In general, I would advise you to only use a ModelForm if you're actually going to save the model, in which case a class attribute is the cleaner approach. Instead of doing all this, if your form isn't going to save the actual model, you should not use a ModelForm but a Form and define all fields and cleaning in the form itself.

dirkgroten
  • 20,112
  • 2
  • 29
  • 42
  • Am I right? : You are stoping the model instance from going into modelform and poping 'exam_date' from it. And the modified model instance will be passed to the modelform building machine. right? – Amin Ba Oct 06 '19 at 13:47
  • No. Just dynamically removing `exam_date` from the list of fields that are checked in the clean method. – dirkgroten Oct 06 '19 at 13:49
  • I am trying to interepret the three lines of your code to learn from it. The first line says whatever that is going into ModelForm should first pass the two lines below. right? – Amin Ba Oct 06 '19 at 13:50
  • I’m just overriding the init method. First init the form (which sets instance) then change the instance. – dirkgroten Oct 06 '19 at 13:54
  • I encountered a key error. This may be because I did not have exclude in my real code. I have fields instead (I modified the question to reflect this). – Amin Ba Oct 06 '19 at 14:04
  • In that case just pop the fields explicitly that you need to remove – dirkgroten Oct 06 '19 at 14:05
  • Can you please edit your answer based on your last recommendation and the new versin of my question? – Amin Ba Oct 06 '19 at 14:06
  • Just pop the non used fields one by one like I do for exam_date – dirkgroten Oct 06 '19 at 14:09
  • There are no other fields to pop in your question. So I can’t change my answer but this is really trivial. Please learn python. – dirkgroten Oct 06 '19 at 14:10
  • I aksed my question here: https://stackoverflow.com/questions/58258213/how-to-remove-a-field-from-modelform-model-instance – Amin Ba Oct 06 '19 at 14:28
  • But doesn’t pop(“ielts_exam_data”) work? What’s the KeyError you’re getting. – dirkgroten Oct 06 '19 at 14:30
  • documentation of you answer in here: https://stackoverflow.com/questions/58258213/how-to-remove-a-field-from-modelform-model-instance Thank you very much. I hope I can give back to community – Amin Ba Oct 06 '19 at 17:02
  • You are right. I am using StudentIeltsFilterForm to make use of models and make it easier to build a form. I have to change them all to form, instead of modelform – Amin Ba Oct 06 '19 at 17:14
  • 1
    Class vs instance attribute https://dzone.com/articles/python-class-attributes-vs-instance-attributes – Amin Ba Oct 06 '19 at 17:19