15

I have an Abstract base model and 2 inheriting models, and I need to force the related_name to be in a specific format.

class Animal(models.Model):
    legs = models.IntegerField(related_name='%(class)s')
    habitat = models.ForeignKey(Habitats, related_name='%(class)s')

class DogAnimal(BaseModel):
    name = models.CharField(max_length=20, related_name='dog_animal')

class CatAnimal(BaseModel):
    name = models.CharField(max_length=20, related_name='cat_animal')

Generally, related_name = '%(class)s' will result in catanimal and doganimal respectively.

I need underscored values like this: dog_animal, cat_animal

Here is the 'Why' I need to do this - Legacy. These models were not organized with a base class - so the related_name originally specified was 'dog_animal' and 'cat_animal'. Changing this would be a lot of work.

rtindru
  • 5,107
  • 9
  • 41
  • 59
  • 2
    You can’t easily change behavior of ``"%(class)s"`` there, I’m afraid. Consider grepping through your code over all occurrences of, e.g., ``dog_animal`` and ``cat_animal`` and replacing them appropriately, or renaming classes in an un-pythonic but backwards-compatible way to Dog_Animal and Cat_Animal. – Anton Strogonoff Jul 24 '15 at 20:07
  • 2
    How come you have a `related_name` for `IntegerField` and `CharField`? Also, why your models inherit from `BaseModel`, while your abstract base model seems to be named `Animal`? – Antoine Pinsard Oct 17 '18 at 09:20

2 Answers2

13

A solution might be not to specify the related_name for habitat and define a default_related_name for all children:

class Animal(models.Model):

    class Meta:
        abstract = True

    habitat = models.ForeignKey(Habitats, on_delete=models.CASCADE)


class DogAnimal(Animal):

    class Meta:
        default_related_name = 'dog_animal'


class CatAnimal(Animal):

    class Meta:
        default_related_name = 'cat_animal'
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87
  • Nice, I didn't realise this Meta option existed. [See Django docs](https://docs.djangoproject.com/en/dev/ref/models/options/#default-related-name) – DrMeers Feb 04 '22 at 08:45
2

It requires a little tweak, but I think you can do this by overriding the ForeignKey class:

from django.utils.text import camel_case_to_spaces


class MyForeignKey(models.ForeignKey):
    def contribute_to_class(self, cls, *args, **kwargs):
        super().contribute_to_class(cls, *args, **kwargs)

        if not cls._meta.abstract:
            related_name = self.remote_field.related_name
            related_query_name = self.remote_field.related_query_name
            underscore_name = camel_case_to_spaces(cls.__name__).replace(" ", "_")
            if related_name:
                self.remote_field.related_name = related_name.format(
                    underscore_name=underscore_name
                )
            if related_query_name:
                self.remote_field.related_query_name = related_query_name.format(
                    underscore_name=underscore_name
                )


class Animal(models.Model):
    class Meta:
        abstract = True

    habitat = MyForeignKey(
        Habitats, on_delete=models.CASCADE, related_name="{underscore_name}"
    )
tony
  • 870
  • 7
  • 16
Antoine Pinsard
  • 33,148
  • 8
  • 67
  • 87