Home > Enterprise >  Django sales campaign model with list of foreign keys that can only appear in one sales campaign?
Django sales campaign model with list of foreign keys that can only appear in one sales campaign?

Time:09-23

I want to create a sales campaign which stores a list of books (Items). But the book should only be applied to one sales campaign — ie. it should not be able to appear in two campaigns.

I have the following model:

class Campaign(models.Model):

    campaign_name = models.CharField(max_length=50, null=False, blank=False, default='Special Offer')
    included_items = models.ManyToManyField(Item, blank=True)
    active = models.BooleanField(default=True)
    fixed_price = models.DecimalField(max_digits=6, blank=True, null=True, decimal_places=2)

But I think the included_items field might be of the wrong field type. From reading other questions and the Django manual, I think I may be approaching this back to front. Perhaps I should be tackling this from the Item model instead? (Shown below for reference)

class Item(models.Model):

    sku = models.CharField(
        max_length=10, null=True, blank=True, 
        default=create_new_sku)
    title = models.CharField(max_length=254)
    genre = models.ManyToManyField('Genre', blank=True)
    author = models.ManyToManyField('Author', blank=True)
    description = models.TextField()
    age_range = models.ManyToManyField(
        'Age_range')
    image_url = models.URLField(max_length=1024, null=True, blank=True)
    image = models.ImageField(null=True, blank=True)
    price = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
    discount = models.DecimalField(max_digits=2, decimal_places=0, default=0)
    set_sale_price = models.DecimalField(max_digits=6, decimal_places=2, default=0.00)
    original_sale_price = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
    final_price = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=False, editable=False)


CodePudding user response:

If each item can only belong to one campaign you should be using a ForeignKey to campaign in the Item model to create your many-to-one relationship

CodePudding user response:

Actually your design is fine.

Using a ForeignKey from @Henty's answer on the Item model still runs into issues where if a campaign becomes inactive (active=False), the Item model needs to nullify the ForeignKey to allow other campaigns to offer it. In addition, an Item model's data should only have information about that instance. To me I think it's weird having a foreign key to a campaign in an Item model since that data is irrelevant to a book. You probably want to read up on data normalization.

That being said, you should manually specify your many-to-many intermediary table as you'll need to apply a unique constraint in it so that only one active campaign can offer the book.

This intermediary table will also have fields such as the discount or sale_price of the book since different campaigns can offer different discounts/prices. I'm assuming price in your Item model is the base retail price on your book so discount, set_sale_price, original_sale_price, and final_price are not needed - again, refer back to data normalization.

In addition, you will need to copy over the active field from your Campaign instance to your intermediary table because it will be needed for the unique constraint. This is done by overriding the save method on your intermediary table to copy over the active value from the Campaign instance.

You'll also have to override the save method on your Campaign model to update the active field on your intermediary instances when you activate/deactivate a campaign.

class Campaign(models.Model):
    campaign_name = ...
    included_items = models.ManyToManyField(
        Item,
        through='CampaignItem',
        through_fields=('campaign', 'item')
    )
    active = ...
    fixed_price = ...

    def save(self, *args, **kwargs):
        # pre-save, update associated campaign-item instances
        CampaignItem.objects.filter(campaign=self).update(active=self.active)

        super().save(*args, **kwargs)


class CampaignItem(models.Model):
    campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE)
    item = models.ForeignKey(Item, on_delete=models.CASCADE)
    active = models.BooleanField(editable=False)
    discount = ...
    sale_price = ...
    # add any other relevant fields related to this campaign item

    class Meta:
        # add constraint where item can only be in one active campaign
        constraints = [
            UniqueConstraint(fields=['item'],
                condition=Q(active=True),
                name='unique_active_item')
        ]

    def save(self, *args, **kwargs):
        # copy over the `active` value from campaign
        self.active = self.campaign.active
        super().save(*args, **kwargs)
    
  • Related