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)