0

I would like to define a Django Model looking like this:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class Foo(models.Model):
    object_id_1 = models.UUIDField()
    content_type_1 = models.ForeignKey(ContentType)
    object_id_2 = models.UUIDField()
    content_type_3 = models.ForeignKey(ContentType)
    object_id_3 = models.UUIDField()
    content_type_3 = models.ForeignKey(ContentType)
    # etc.
    d1 = GenericForeignKey("content_type_1", "object_id_1")
    d2 = GenericForeignKey("content_type_2", "object_id_2")
    d3 = GenericForeignKey("content_type_3", "object_id_3")
    # etc.

Obviously, the more dimensions (d stands for "dimension") I add, the messier it gets: this isn't very DRY, all the more since I've removed the many fields options in this example.

Is there a way to dynamically define these model attributes, for instance in a for loop, without using eval? If not, what would be the cleanest and safest way to resort to eval here?

I've found a very similar Stackoverflow question here but it is more specific and it hasn't got any generic answer.

scūriolus
  • 657
  • 2
  • 5
  • 15
  • 1
    If you have N number of these relations, why don't you just create another model with a foreign key to this and the generic foreign key to the other models? Also no need for eval, you could make use of `type` to dynamically create classes if you need to. – Abdul Aziz Barkat Nov 24 '22 at 11:51
  • Could you please illustrate what you have in mind? I’m not sure to get it. Thanks – scūriolus Nov 24 '22 at 11:58

1 Answers1

0

My solution

I'm not entirely satisfied with this piece of code because it means relying on undocumented internals which have no stability guarantee whatsoever, but this is what I've finally done, for future readers:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class Foo(models.Model):
    NB_DIMENSIONS = N
    pass

# outside of the class
for i in range(1, Foo.NB_DIMENSIONS + 1):
    Foo.add_to_class(f"object_id_{i}", models.UUIDField())
    Foo.add_to_class(f"content_type_{i}", models.ForeignKey(ContentType))
    Foo.add_to_class(f"d{i}", GenericForeignKey(f"content_type_{i}", f"object_id_{i}"))

Here, I'm indirectly using the Managers contribute_to_class hook as described in this old Stackoverflow question. It is mentioned here as well. One should pay attention to the fact that this method is an undocumented, private, internal API. Django does not provide backwards-compatibility guarantees for it, and if you make use of it you accept the risk that it might change or break at any time.

Alternative 1

The following code seems to work as well, but note that it's a bad idea to modify locals in Python:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class Foo(models.Model):
    NB_DIMENSIONS = N
    for i in range(1, NB_DIMENSIONS + 1):
        locals()[f"object_id_{i}"] = models.UUIDField()
        locals()[f"content_type_{i}"] = models.ForeignKey(ContentType)
        locals()[f"d{i}"] = GenericForeignKey(f"content_type_{i}", f"object_id_{i}")

Let me insist on this: using locals() may be a bad idea for various reasons, even if it seems more readable.

Alternative 2

As suggested by @Abdul Aziz Barkat above, it should be possible be to create a intermediate class with type() (cf. this Stackoverflow thread). This has its downsides: that implies one more class to manage and I believe it's harder to understand and to maintain for less experienced developers.

scūriolus
  • 657
  • 2
  • 5
  • 15