How to make field case insensitive ?

Model Coupon is defined by 4 characters unique code.
I want to make code case insensitive.
When coupons are applied, I can very simply compare using __iexact.

But when using Django Admin to define new coupons, I can store TEST and TeSt as separate records.
I expect that saving TeSt will fail in case TEST record already exists.

So how to make field case insensitive ?

Is there an simple/elegant way to achieve this ?

# models.py:
class Coupon(models.Model):
    code = models.CharField(max_length=4,unique=True)
    # ... other code ...

# admin.py:
class CouponAdmin(admin.ModelAdmin):
    fields = ['code','description','valid_from','valid_to']
	# ... other code ...

At least two options come to mind:

  • Store all codes as upper case, convert all entered code fields to upper case before comparison or saving. Use a Unique Constraint for that field to prevent duplicate entries.

  • Create a custom CheckConstraint that would check to see if another row exists in the database with a case-insensitive match. (Never tried this, don’t know how well it would work, would probably not perform as well as the first option)

There may be other options, but I’m not thinking of anything else at the moment.

2 Likes

How can I implemented the first suggestion correctly ?

class Coupon(models.Model):
    code = models.CharField(max_length=10,unique=True)

I added:

    def save(self, *args, **kwargs):
        if Coupon.objects.filter(code__iexact=self.code).first():
            raise ValidationError("Invalid code - this code already exists.")
        else:
            self.code = self.code.upper()
            super(Coupon, self).save(*args, **kwargs)

But unfortunately, when it comes to raise ValidationError("Invalid code - this code already exists.") validation message is not shown by the field in my Django Admin like it is for regular unique=True but I get error page with validation message.

See the docs on constraints and UniqueConstraint for how to define a UniqueConstraint on a model.

1 Like

Was not able to make it work with UniqueConstraint, at least not in Django Admin - saving was failing with page error instead of showing error near the field to correct it.

So I did this:

def validate_coupon_code_caseinsensitive(value):
    if Coupon.objects.filter(code__iexact=value).exists():
        raise ValidationError('This CODE already exists.')
    return value

class Coupon(models.Model):
    code = models.CharField(max_length=10, unique=True, validators=[validate_coupon_code_caseinsensitive])
    # ... other code ...
    def save(self, *args, **kwargs):
        self.code = self.code.upper()
        super(Coupon, self).save(*args, **kwargs)
    # ... other code ...

This way it is converting to uppercase when saving.
But it is also doing the validation in Django Admin before running save part.

As a general principle, I’d keep both.

Yes, absolutely, the field-level validation in the form provides UX benefits. However, the UniqueConstraint provides a more “global” enforcement, should you ever create a different form / view to edit that field.

1 Like

Tried by adding CheckConstraints but that never worked.

My Attempt

constraints = [
    CheckConstraint(check=Q(code=Lower(F('code'))))
]

this worked for me (django 4.1):

from django.db import models
from django.db.models.functions import Upper

class Category(models.Model):
    name = models.CharField(max_length=60, unique=True, verbose_name=_('name')) # not sure if unique param is need here

    class Meta:
        constraints = [
            models.UniqueConstraint(Upper('name'), name='unique_upper_name_category')
        ]


Also, from docs:

Changed in Django 4.1:

In older versions, constraints were not checked during model validation.

one of the best options for do this is that create a form and past it exept of the admin form .
like this :

from . Import YourModel
from django import forms
from django.contrib import admin
class HandMadeFormForAdminPage(forms.ModelForm):
    class Meta:
        model = YourModel
        fields = "__all__"
    def clean(self):
        cleaned_data = super().clean()

        field_value = self.cleaned_data.get("YourFieldName")
        value_pair  = YourModel.objects.filter(YourModelFieldName__iexact=field_value).first()
        if value_pair:
            self.add_error("YourFieldName", f"the {value_pair} alredy exist")
class YourModelAdmin(admin.ModelAdmin):
       form =  HandMadeFormForAdminPage