Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django - conditional foreign key

Tags:

python

orm

django

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')
  • If account_type = 1 then link to brand model
  • If account_type = 2 then link to creator model
  • If account_type = 3 then link to agency model

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
like image 969
methuselah Avatar asked May 05 '18 07:05

methuselah


Video Answer


1 Answers

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.

like image 80
rtindru Avatar answered Sep 28 '22 08:09

rtindru