0

I asked a question and, as I have read a little, I now can better express what I need: How to do model level custom field validation in django?

I have this model:

class StudentIelts(Model):

    SCORE_CHOICES = [(i/2, i/2) for i in range(0, 19)]

    student = OneToOneField(Student, on_delete=CASCADE)
    has_ielts = BooleanField(default=False,)
    ielts_listening = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_reading = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_writing = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_speaking = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )

and have this model form:

class StudentIeltsForm(ModelForm):

    class Meta:
        model = StudentIelts
        exclude = ('student')

    def clean(self):
        cleaned_data = super().clean()
        has_ielts = cleaned_data.get("has_ielts")

        if has_ielts:
            msg = "Please enter your score."
            for field in self.fields:
                if not self.cleaned_data.get(str(field)):
                    self.add_error(str(field), msg)

        else:
            for field in self.fields:
                self.cleaned_data[str(field)] = None
                self.cleaned_data['has_ielts'] = False

        return cleaned_data

What I am doing here is that checking if has_ielts is True, then all other fields should be filled. If has_ielts is True and even one field is not filled, I get an error. If has_ielts is False, an object with has_ielts=False and all other fields Null should be saved. I now want to do it on the model level:

class StudentIelts(Model):

    SCORE_CHOICES = [(i/2, i/2) for i in range(0, 19)]

    student = OneToOneField(Student, on_delete=CASCADE)
    has_ielts = BooleanField(default=False,)
    ielts_listening = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_reading = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_writing = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )
    ielts_speaking = FloatField(choices=SCORE_CHOICES, null=True, blank=True, )

    def clean(self):
    # I do not know what to write in here

and have this model form:

class StudentIeltsForm(ModelForm):

    class Meta:
        model = StudentIelts
        exclude = ('student')

In the clean method of my model I want something with this logic(this is psedue code):

def clean(self):
    msg = "Please enter your score."

    if self.has_ielts:
        my_dic = {}
        for f in model_fields:
             if f is None:
                  my_dic.update{str(field_name): msg}
        raise ValidationError(my_dic)

How can I do this?
How can I get the same result as my modelform but at the model level?

Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
Amin Ba
  • 1,603
  • 1
  • 13
  • 38
  • You need to explicitly go through the 4 fields one by one (also in your form), since you're only interested in the fields starting with `ielts`. It's bad coding to include the other fields. `if not ielts_listening: raise ValidationError(...); ...`. – dirkgroten Oct 01 '19 at 14:21
  • I want to have a reusable code to use in other models such GRE, GMAT, IBT, etc, In my current modelform, I am going through all fields and changing back one of them. There should be a neat way to do it while having a reusable code. Doesn't this help? : ```for field in self._meta.fields:``` – Amin Ba Oct 01 '19 at 14:30
  • 1
    but that includes fields like 'id', 'student' and possibly other fields you add later. You could just define the list of fields that needs to be non-empty together as a class attribute and cycle through that list. Not doing that will give unpredictable results for other developers using your models. – dirkgroten Oct 01 '19 at 14:38
  • @Daniel I have encountered an issue related to this question, available in here: https://stackoverflow.com/questions/58254333/how-to-handle-the-validation-of-the-model-form-when-the-model-has-a-clean-method – Amin Ba Oct 06 '19 at 05:03

1 Answers1

1

You need to explicitly declare the fields that should be non-empty, otherwise you're cycling through fields that are not related to your clean method, like id and student. Someone might want to add a field later one that's not mandatory and wonder why it raises a validation error.

class StudentIelts(Model):

    # fields

    non_empty_fields = ['ielts_reading', ...]

    def clean(self):
        errors = {}
        if self.has_ielts:
            for field_name in self.non_empty_fields:
                if not getattr(self, field_name):
                    errors[field_name] = msg
        if errors:
            raise ValidationError(errors)

dirkgroten
  • 20,112
  • 2
  • 29
  • 42
  • You are right!! I had not thought of it!!! Your solution is neat and beautiful!! What does this part do ? ```if not getattr(self, field_name):``` – Amin Ba Oct 01 '19 at 14:49
  • 1
    this is python's method for actually accessing attributes (and methods) on an object: `getattr(self, "ielts_reading")` is the same as `self.ielts_reading`. See [this question](https://stackoverflow.com/questions/4075190/what-is-getattr-exactly-and-how-do-i-use-it) for more. – dirkgroten Oct 01 '19 at 14:50
  • 1
    for example, Django uses it to clean individual fields where you defined the `clean_FOO` method: `getattr(self, f"clean_{field_name}")()` will actually get the method and call it (due to the `()`) – dirkgroten Oct 01 '19 at 14:54
  • So, self.ielts_reading actually is what is passed in the field of ielts_reading of the form. right? And now I can remove what I have in the clean method of my modelform. right? which one is better? having clean method in model form or in the model? – Amin Ba Oct 01 '19 at 14:55
  • and is there any difference having clean method in model form or in in the model? – Amin Ba Oct 01 '19 at 14:57
  • 1
    I don't understand your first question. But yes, any validation you do in your model you don't need to do in the model form. In general, validation on the model is the first place to start. You add validation in your model form if the business logic at that point requires you to perform additional validation or if you need to validate with variables not available in the model, e.g the current user (request isn't available in the model validation). – dirkgroten Oct 01 '19 at 14:57
  • I asked another question about this StudentIelts model in here: https://stackoverflow.com/questions/58180005/should-i-divide-a-table-by-onetoonefield-if-the-number-of-columns-is-too-many I would appreciate if you share your idea in there about this issue – Amin Ba Oct 01 '19 at 15:00
  • I think the approved answer is good. In my opinion, clarity of code is more important than database optimisation, so yes, you should separate the models as that means you can keep concerns separate. Now, maybe your various "exam" models have some things in common, like this `clean()` method, in which case I'd suggest you to make them all inherit from one abstract model. – dirkgroten Oct 01 '19 at 15:05
  • The non-empty_fields of each one are different having different names. How can they inherit from an abstract model? I do not understand it – Amin Ba Oct 01 '19 at 15:08
  • 1
    In the abstract class you define `non_empty_fields = []` and in each subclass you override it. But that way you don't have to repeat the `clean()` method. – dirkgroten Oct 01 '19 at 15:11
  • If a score of zero is entered, then I have ValidationError. The problem should be with this part: ```if not getattr(self, field_name):``` ```if not 1``` is ```False``` but ```if not 0``` is ```True``` – Amin Ba Oct 01 '19 at 16:00
  • 1
    then check `if getattr(self, field_name) is None` or `if getattr(self, field_name) in [None, False, '']` instead of checking truthiness. – dirkgroten Oct 01 '19 at 16:03
  • Hi. I followed the solution you offered but I have this problem: https://stackoverflow.com/questions/58248378/error-because-modelform-excludes-a-field-of-the-model-having-custom-clean-method – Amin Ba Oct 05 '19 at 16:25
  • I have simplfied the question in here: https://stackoverflow.com/questions/58254333/how-to-override-custom-model-clean-method-if-a-modelform-does-not-include-some-f – Amin Ba Oct 06 '19 at 04:54