ModelAdmin readonly fields make CheckConstraint not checked by Model.validate_contraints and result to IntegrityError

I’ve a field that is used in a CheckContraint :

constraints = [
    CheckConstraint(
        condition=~(Q(field_a="") & Q(field_b="")),
        name="one_is_required",
        violation_error_message=_("One field is required"),
   )
]

The ModelAdmin of the model, set, under conditions field_b as readonly.

def get_readonly_fields(self, request, obj=None):
        """
        Return readonly fields
        """
        readonly_fields = super().get_readonly_fields(request, obj)
        if condition:
            readonly_fields = (*readonly_fields, "field_b")
        return readonly_fields

When the field is readonly, it is excluded from Model.full_clean and so from Model.validate_constraints.
So the validation doesn’t occur django side, and it crash at DB side with IntegrityError.

This issue is closely related to the one discussed here: Fields excluded from model constraint validation

This is the same issue discussed here.

The Form cannot validate any field which is not part of it, thus you need to find a way to add the field to the form and yet to prevent users from editing it, this may be achieved by a custom field which displays the value and adds an hidden input for it.

This:

Does not prevent this:

As users are always able to edit the html of a page and can change the value directly.

The more appropriate solution would be to create a custom form for that ModelAdmin class that performs the necessary test within the form.

In the custom form this can be done by checking if the pseudo-readonly field is in form.changed_data, which means that someone is trying to tamper that value. This should raise a validation error.

Quite true - but if you’re already creating a custom form, you’re still better off not including that value at all. (Why create even the opportunity for mischief?)

Thanks for all your responses.
I’ve finally opted to override Model.full_clean so it enforce that fields from constraints can only be excluded altogether, or not excluded at all.

def full_clean(self, *args, exclude=None, **kwargs):
    exclude = set(exclude)
    constraint_fields = {"field_a", "field_b"}
    if intersection := constraint_fields.intersection(exclude):
        if len(intersection) < len(constraint_fields):
            exclude -= constraint_fields
    super().full_clean(*args, exclude=exclude, **kwargs)

It seems enough to my use case but maybe I miss something