methuselah
methuselah

Reputation: 13206

Django - conditional foreign key

I have the following 4 models in my program - User, Brand, Agency and Creator.

User is a superset of Brand, Agency and Creator.

A user can be a brand, agency or creator. They cannot take on more than one role. Their role is defined by the accountType property. If they are unset (i.e. 0) then no formal connection exists.

How do I express this in my model?

User model

class User(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    email = models.EmailField(max_length=255, null=True, default=None)
    password = models.CharField(max_length=255, null=True, default=None)
    ACCOUNT_CHOICE_UNSET = 0
    ACCOUNT_CHOICE_BRAND = 1
    ACCOUNT_CHOICE_CREATOR = 2
    ACCOUNT_CHOICE_AGENCY = 3
    ACCOUNT_CHOICES = (
        (ACCOUNT_CHOICE_UNSET, 'Unset'),
        (ACCOUNT_CHOICE_BRAND, 'Brand'),
        (ACCOUNT_CHOICE_CREATOR, 'Creator'),
        (ACCOUNT_CHOICE_AGENCY, 'Agency'),
    )
    account_id = models.ForeignKey(Brand)
    account_type = models.IntegerField(choices=ACCOUNT_CHOICES, default=ACCOUNT_CHOICE_UNSET)

    class Meta:
        verbose_name_plural = "Users"

    def __str__(self):
        return "%s" % self.email

Brand model

class Brand(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    name = models.CharField(max_length=255, null=True, default=None)
    brand = models.CharField(max_length=255, null=True, default=None)
    email = models.EmailField(max_length=255, null=True, default=None)
    phone = models.CharField(max_length=255, null=True, default=None)
    website = models.CharField(max_length=255, null=True, default=None)

    class Meta:
        verbose_name_plural = "Brands"

    def __str__(self):
        return "%s" % self.brand

Creator model

class Creator(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    first_name = models.CharField(max_length=255, null=True, default=None)
    last_name = models.CharField(max_length=255, null=True, default=None)
    email = models.EmailField(max_length=255, null=True, default=None)
    youtube_channel_username = models.CharField(max_length=255, null=True, default=None)
    youtube_channel_url = models.CharField(max_length=255, null=True, default=None)
    youtube_channel_title = models.CharField(max_length=255, null=True, default=None)
    youtube_channel_description = models.CharField(max_length=255, null=True, default=None)
    photo = models.CharField(max_length=255, null=True, default=None)
    youtube_channel_start_date = models.CharField(max_length=255, null=True, default=None)
    keywords = models.CharField(max_length=255, null=True, default=None)
    no_of_subscribers = models.IntegerField(default=0)
    no_of_videos = models.IntegerField(default=0)
    no_of_views = models.IntegerField(default=0)
    no_of_likes = models.IntegerField(default=0)
    no_of_dislikes = models.IntegerField(default=0)
    location = models.CharField(max_length=255, null=True, default=None)
    avg_views = models.IntegerField(default=0)
    GENDER_CHOICE_UNSET = 0
    GENDER_CHOICE_MALE = 1
    GENDER_CHOICE_FEMALE = 2
    GENDER_CHOICES = (
        (GENDER_CHOICE_UNSET, 'Unset'),
        (GENDER_CHOICE_MALE, 'Male'),
        (GENDER_CHOICE_FEMALE, 'Female'),
    )
    gender = models.IntegerField(choices=GENDER_CHOICES, default=GENDER_CHOICE_UNSET)

    class Meta:
        verbose_name_plural = "Creators"

    def __str__(self):
        return "%s %s" % (self.first_name,self.last_name)

Agency model

class Agency(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    name = models.CharField(max_length=255, null=True, default=None)
    agency = models.CharField(max_length=255, null=True, default=None)
    email = models.EmailField(max_length=255, null=True, default=None)
    phone = models.CharField(max_length=255, null=True, default=None)
    website = models.CharField(max_length=255, null=True, default=None)

    class Meta:
        verbose_name_plural = "Agencies"

    def __str__(self):
        return "%s" % self.agency

Update:

So I've whittled it down to this bit here in the model:

ACCOUNT_CHOICE_UNSET = 0
ACCOUNT_CHOICE_BRAND = 1
ACCOUNT_CHOICE_CREATOR = 2
ACCOUNT_CHOICE_AGENCY = 3
ACCOUNT_CHOICES = (
    (ACCOUNT_CHOICE_UNSET, 'Unset'),
    (ACCOUNT_CHOICE_BRAND, 'Brand'),
    (ACCOUNT_CHOICE_CREATOR, 'Creator'),
    (ACCOUNT_CHOICE_AGENCY, 'Agency'),
)
account_type = models.IntegerField(choices=ACCOUNT_CHOICES, default=ACCOUNT_CHOICE_UNSET)
limit = models.Q(app_label='api', model='Brand') | \
        models.Q(app_label='api', model='Creator') | \
        models.Q(app_label='api', model='Agency')
content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices(), related_name='user_content_type')
content_object = GenericForeignKey('content_type', 'email')

How do I accomplish this? Getting this error:

  File "/Users/projects/adsoma-api/api/models.py", line 7, in <module>
    class User(models.Model):
  File "/Users/projects/adsoma-api/api/models.py", line 28, in User
    content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices(), related_name='user_content_type')
NameError: name 'get_content_type_choices' is not defined

Upvotes: 6

Views: 5236

Answers (2)

Fathy
Fathy

Reputation: 413

Here is how I solve it, override the save method check your condition tested on Django 3.2

CUSTOMER = [
    ('customer', 'customer'),
    ('supplier', 'supplier'),
]


class Customer(models.Model):
    name = models.CharField(max_length=256, null=False, blank=False)

    user_type = models.CharField(max_length=32, choices=CUSTOMER)


class SupplierOrder(models.Model):
    price = models.FloatField(default=0)
    supplier = models.ForeignKey(Customer, related_name='supplier', on_delete=models.CASCADE)

    def save(self, *args, **kwargs):
        supplier = get_object_or_404(Customer, id=self.supplier.id)
        if supplier.user_type != 'supplier':
            raise ValueError('selected user must be supplier')
        super().save(*args, **kwargs)

Upvotes: 0

rtindru
rtindru

Reputation: 5337

Have you tried exploring Django's GenericForeignKey field?

class User(models.Model):
    ...
    limit = models.Q(app_label='your_app_label', model='brand') | 
            models.Q(app_label='your_app_label', model='creator') | 
            models.Q(app_label='your_app_label', model='agency')
    content_type = models.ForeignKey(ContentType, limit_choices_to=limit, related_name='user_content_type')
    object_id = models.UUIDField()
    content_object = GenericForeignKey('content_type', 'object_id')

You can access the User's brand/creator/agency by using the following notation:

User.objects.get(pk=1).content_object

This will be an instance of Brand/Creator/Agency as per the content_type.

https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey

Update based on your comment

Re 1: Using email:

class User(models.Model):
    ...
    email = models.EmailField(max_length=255, unique=True)
    limit = models.Q(app_label='your_app_label', model='brand') | 
            models.Q(app_label='your_app_label', model='creator') | 
            models.Q(app_label='your_app_label', model='agency')
    content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices, related_name='user_content_type')
    content_object = GenericForeignKey('content_type', 'email')

Note: Email can not be a nullable field anywhere if you follow this approach! Also this approach seems hacky/wrong since the email field is now declared in multiple places; and the value can change if you re-assign the content objects. It is much cleaner to link the GenericForeignKey using the discrete UUIDField on each of the other three models

Re 2: Using account_type field:

ContentType is expected to be a reference to a Django Model; therefore it requires choices that are Models and not integers. The function of limit_choices_to is to perform a filtering such that all possible models are not surfaced as potential GenericForeignKey

class ContentType(models.Model):
    app_label = models.CharField(max_length=100)
    model = models.CharField(_('python model class name'), max_length=100)
    objects = ContentTypeManager()

However, limit_choices_to does accept callables; so you can write a helper method that translates your account_type to the correct Model

I am not clear about how this transaltion should work; so I leave that to you.

Upvotes: 7

Related Questions