I have a Scheme model that can have 2 rewards assign to it only, one for a member of the scheme and the other for their friend.
Below is how I have design the model for this, but now I'm starting to question the design, Is the link to Scheme and rewards incorrect? Should I have the relationship the other way around on abstract reward instead?
Scheme:
class Scheme(models.Model):
name = models.CharField(max_length=60)
participant_reward_content_type = models.ForeignKey(ContentType,
editable=False,
related_name='%(app_label)s_%(class)s_as_participant',
null=True, blank=True
)
participant_reward_object_id = models.PositiveIntegerField(null=True, blank=True)
participant_reward = generic.GenericForeignKey('participant_reward_content_type', 'participant_reward_object_id')
friend_reward_content_type = models.ForeignKey(ContentType,
editable=False,
related_name='%(app_label)s_%(class)s_as_friends',
null=True, blank=True
)
friend_reward_object_id = models.PositiveIntegerField(null=True, blank=True)
friend_reward = generic.GenericForeignKey('friend_reward_content_type', 'friend_reward_object_id')
Rewards:
class AbstractReward(models.Model):
"""
Abstract reward common information shared for all rewards.
"""
description = models.CharField(max_length="150")
active = models.BooleanField(default=True)
#scheme = models.ForeignKey(Scheme, null=True,)
class Meta:
abstract = True
class SingleVoucherReward(AbstractReward):
"""
Single-use coupons are coupon codes that can only be used once
"""
pass
class Meta:
app_label = 'schemes'
class MultiVoucherReward(AbstractReward):
"""
A multi-use coupon code is a coupon code that can be used unlimited times.
"""
code = models.CharField(max_length=200)
expiry = models.DateTimeField(null=True)
class Meta:
app_label = 'schemes'
class CustomReward(AbstractReward):
"""
A reward class used when it can't be handled or they would like to
handle reward fulfillment themselves.
"""
pass
class Meta:
app_label = 'schemes'
I would recommend keeping it really simple - http://en.wikipedia.org/wiki/KISS_principle
Given the similarity in data definitions of the 3 types of Reward I'd lose the inheritance altogether and just give it a type selection:
class Reward(models.Model):
SINGLE = 'Single'
MULTI = 'Multi'
CUSTOM = 'Custom'
TYPE_CHOICES = (
(SINGLE, 'Single'),
(MULTI, 'Multi'),
(CUSTOM, 'Custom'),
)
description = models.CharField(max_length="150")
active = models.BooleanField(default=True)
type = models.CharField(max_length=10, choices=TYPE_CHOICES, default=SINGLE)
code = models.CharField(max_length=200, blank=True)
expiry = models.DateTimeField(null=True)
Two Scoops of Django - which is a great reference for how to approach things in Django - also recommends this approach.
This also means that you don't need the GenericForeignKey and can have simple foreign keys, massively reducing the complexity again:
class Scheme(models.Model):
name = models.CharField(max_length=60)
participant_reward = models.ForeignKey('Reward', null=True, blank=True)
friend_reward = models.ForeignKey('Rewards', null=True, blank=True)
Built in stuff like the Django admin and ModelForms will just work out of the box with this approach.
Some may not like the verbosity of the TYPE_CHOICES but it is so simple and clear to maintain.
I also realize that you may end up with methods on the Reward class that have to modify behaviour for different types e.g.:
if self.type = CUSTOM:
pass
but again this is very simple to maintain. You could use Proxy Models if the code starts to really diverge.
Some may argue that this is not 'Pythonic' but we are not handling pure python classes here, and besides the Zen of Python states as its third principle:
Simple is better than complex.
You can make your AbstractReward not so abstract (and rename it to BaseReward), then ForeignKey to it and get actual reward type and object in some method. You will need to make an additional request, but I think it will be the same with GenericForeignKey.
Related
I am developing a simple restaurant app in django and mysql and after days of googling and research, i wasn't able to find a suitable answere for this particular problem, here is my django Model
class MenuItem(models.Model):
menu_category = models.ForeignKey(MenuCategory, to_field='slug', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
image = models.ImageField(upload_to='uploads/menu/')
slug = models.SlugField(null=True, blank=True, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
and here is the problem:
for example i have a menuItem "MEXICAN BURGER" i would like to ask the user their choice of meat.. ie either chicken or beef, or their choice of bread, white or brown,
or i might be having a MenuItem "OMELETTE COMBO" and i would like to ask the user the type of omellete they'd like to have eg ('spanish omelete', 'spinach and mushroom omelete')
or i might be having a MenuItem "ESPRESSO" and i would like them to choose between ('single', 'double')
*one menuitem can have multitple choices related to it, ie burger item can have choice of bread as well as choice of meat
for a better understanding of my problem visit this
link
*
and this another link
You can create another model as needed e.g. MeatOption and add to it a ForeignKey to MenuItem:
class MeatOption(models.Model):
meat_type = models.CharField(max_length=200)
class MenuItem(models.Model):
meat_type = models.ForeignKey(MeatOption, null=True, blank=True)
# rest of fields
Ideally you should think of categorizing your items and reflect that categorization into your models. i.e. you could have class Burgers, class Beverages and so on, so that you only include the relevant options.
And if there is common functionality among all those categories you could inherit that from a Base MenuItem class like so:
class MenuItem(models.Model):
# common fields across all items go here
class Burger(MenuItem):
# burger-specific fields here
meat_type = models.ForeignKey(MeatOption)
class Omelette(MenuItem):
# omelette-specific fields here
ingredient = models.ManyToManyField(Ingredient)
class Ingredient(models.Model):
name = models.CharField(max_length=200)
the details depend on your use-case of course
There are different solutions to this.
Code Based: Inheritance
You create Mixins for each of your choice type and subclass MenuItem and these mixins.
from django.db.models import CharField, TextChoices, Model
class MeatChoices(TextChoices):
CHICKEN = 'chi'
BEEF = 'bee'
class MeatMixin(Model):
meat_choices = CharField(max_length=3, choices=MeatChoices.choices, default=MeatChoices.CHICKEN)
class MeatMenuItem(MeatMixin, MenuItem):
pass
This will require code changes whenever more choices are added to the menu or the menu is changed in that matter.
Data Based: Generic Models
To allow the admin user to create and add choices to the menu items via the Django Admin, you need to create a model structure that enables specifying choices:
from django.contrib.postgres.fields import ArrayField
from django.db.models import ManyToManyField, CharField, Model
class Ingredient(Model):
name = CharField(max_length=50, unique=True)
class IngredientChoiceGroup(Model):
name = CharField(max_length=50, unique=True)
# simple solution for postgres only:
# ingredients = ArrayField(CharField(max_length=50))
# if not postgres or you need more attributes to ingredient:
ingredients = ManyToManyField(Ingredient)
class MenuItem(models.Model):
menu_category = models.ForeignKey(MenuCategory, to_field='slug', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
image = models.ImageField(upload_to='uploads/menu/')
slug = models.SlugField(null=True, blank=True, unique=True)
price = models.DecimalField(max_digits=9, decimal_places=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
ingredient_choices = ManyToManyField(IngredientChoiceGroup)
def __str__(self):
return self.name
This method would allow to configure multiple choice groups per menu item, e.g. "BREAD" and "MEAT". And you can do that all via the Django Admin.
You might need to work out the details. This is just a draft.
Note about ArrayField: it is a simpler structure in the DB, but for the Admin you will need to add a specific widget via 3rd party library or at least write your own parser. In the end, M2M field might be the better choice.
Let me start by stating that I have looked at django-polymorphic for this and still have questions. I have item models and many subtypes for items. Currently, my models look like this.
class Item(models.Model):
pass
class Television(Item):
upc = models.CharField(max_length=12, null=True)
title = models.CharField(max_length=255, null=True)
brand = models.ForeignKey(Brand)
screen_size = models.IntegerField()
class Fridge(Item):
upc = models.CharField(max_length=12, null=True)
title = models.CharField(max_length=255, null=True)
brand = models.ForeignKey(Brand)
stuff_about_fridge = models.CharField(max_length=255, null=True)
I did this at first because then I wouldn't worry about all of the left joins when querying different item types that would be caused if my models looked like this:
class Item(models.Model):
upc = models.CharField(max_length=12, null=True)
title = models.CharField(max_length=255, null=True)
brand = models.ForeignKey(Brand)
class Television(Item):
screen_size = models.IntegerField()
class Fridge(Item):
stuff_about_fridge = models.CharField(max_length=255, null=True)
I am now reaching a point where I realize that I very often query all of the Item models together and have to left join information from the subtypes instead, so I am not really saving myself there. My question is, even if I used something like django-polymorphic, would it make sense to A) put everything that is shared in the parent model and just specific things in the child models or to B) have it like I do where everything is in the child model, but they share a parent model PK just so that they can be queried together?
"Abstract base classes are useful when you want to put some common information into a number of other models. This model will then not be used to create any database table. Instead, when it is used as a base class for other models, its fields will be added to those of the child class"
Reference: https://docs.djangoproject.com/en/3.0/topics/db/models/#abstract-base-classes
The point of using inheritance is for sharing common info as mentioned in option (A).
Django-polymorphic makes using inherited models easier, nothing more.
Reference: https://django-polymorphic.readthedocs.io/en/stable/
That means, in conclusion, option (A) is correct even though if you use Django-polymorphic.
Below method is the right approach:
class Item(models.Model):
upc = models.CharField(max_length=12, null=True)
title = models.CharField(max_length=255, null=True)
brand = models.ForeignKey(Brand)
class Television(Item):
screen_size = models.IntegerField()
class Fridge(Item):
stuff_about_fridge = models.CharField(max_length=255, null=True)
I am having a bit of trouble with the logic of how this should work so I am hoping it is possible.
I figured out 1 possible solution that is written as an answer below, I will accept it in a few days, but if someone comes up with a better solution, I will negate any answer I post.
Overall I am working on an Apartment Move-Out/Move-In Inspection Application in Django, and in both portions I have universal Locations that must be inspected for each report. I have allowed the InspectionLocations objects to be updated/submitted by clients, which is presenting an issue in how submitted reports should be stored in my Database.
What I want is to use the InspectionLocations table as a blueprint to build an Inspection Report for Move-Ins where the form-fields are generated based on the InspectionLocations objects' location, status, and information attributes/fields.
My issue is right at this point, how do I reference those values as a blueprint to build a report submission when the number of fields in the InspectionLocations can change?
from django.db import models
from apps.units.models import Unit
class Inspections(models.Model):
class Meta:
abstract = True
id = models.AutoField(primary_key=True)
inspection_date = models.DateField()
submitted_by = models.ForeignKey(
'users.CustomUser',
default=None,
null=True,
on_delete=models.SET_NULL,
db_column='submitted_by')
last_update = models.DateTimeField(auto_now=True)
date_added = models.DateTimeField(auto_now_add=True, editable=False)
class MoveInInspections(Inspections):
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, db_column='unit_id')
# should have reference to all InspectionLocation items as reference for submission, how?
class MoveOutInspections(Inspections):
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, db_column='unit_id')
date_notice_given = models.DateField(blank=True, null=True, default=None)
date_vacated = models.DateField(blank=True, null=True, default=None)
# should have reference to all InspectionLocation items as reference for submission, how?
class InspectionLocations(models.Model):
'''
Defualt Inspection Locations are created when a
client is created using code like this:
InspectionLocation.objects.get_or_create(location='Living Room')
InspectionLocation.objects.get_or_create(location='Dining Room')
InspectionLocation.objects.get_or_create(location='Kitchen')
InspectionLocation.objects.get_or_create(location='Bedroom')
InspectionLocation.objects.get_or_create(location='Bathroom')
InspectionLocation.objects.get_or_create(location='Other')
'''
id = models.AutoField(primary_key=True)
location = models.CharField(max_length=50)
status = models.BooleanField(default=None)
information = models.TextField(default=None, blank=True)
I have tried ManyToMany fields and FKs but I cannot seem to get the logic working as anytime an object references an InspectionLocations object it is universally changing data for every report, which is leading be to the idea that I somehow need to use it as a blueprint.
I didn't post this in my question because it was getting long, but my best option so far seems to be to use a Django JSONField (as I am using Postgres), like so:
from django.contrib.postgres.fields import JSONField
class MoveInInspections(Inspections):
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, db_column='unit_id')
data = JSONField()
class MoveOutInspections(Inspections):
unit = models.ForeignKey(Unit, on_delete=models.CASCADE, db_column='unit_id')
date_notice_given = models.DateField(blank=True, null=True, default=None)
date_vacated = models.DateField(blank=True, null=True, default=None)
data = JSONField()
To where I store the values of the InspectionLocations object's in a Dictionary
I have a moderation model :
class ItemModeration(models.Model):
class Meta:
indexes = [
models.Index(fields=['object_id', 'content_type']),
]
unique_together = ('content_type', 'object_id')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
published = models.BooleanField(default=False)
...
A descriptor to attach a moderation object on-the-fly :
class ItemModerationDescriptor(object):
def __init__(self, **default_kwargs):
self.default_kwargs = default_kwargs
def __get__(self, instance, owner):
ctype = ContentType.objects.get_for_model(instance.__class__)
try:
moderation = ItemModeration.objects.get(content_type__pk=ctype.id,
object_id=instance.pk)
except ItemModeration.DoesNotExist:
moderation = ItemModeration(item=instance,**self.default_kwargs)
moderation.save()
return moderation
And a model I want to moderate :
class Product(models.Model):
user = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL)
created = models.DateTimeField(
auto_now_add=True,
blank=True, null=True,
)
modified = models.DateTimeField(
auto_now=True,
blank=True, null=True,
)
name = models.CharField(
max_length=PRODUCT_NAME_MAX_LENGTH,
blank=True, null=True,
)
moderation = ItemModerationDescriptor()
Now I can see a product 'published' state easily :
p=Product(name='my super product')
p.save()
print(p.moderation.published)
-> False
The generic relation is useful because I will be able to search the objects to moderate whatever the type is : it could be products, images, comments.
to_moderate_qs = ItemModeration.objects.filter(published=False)
Now, how can I get a filtered list of published products ?
I would like to do something like this
published_products_qs = Product.objects.filter(moderation__published=True, name__icontains='sony')
But, of course, it won't work as moderation attribute is not a Django model field.
How can I do that efficiently ? I am thinking a about an appropriate JOIN, but I cannot see how to do that with django without using raw SQL.
Django has a great built in answer for this: the GenericRelation. Instead of your descriptor, just define a generic relation on your Product model and use it as a normal related field:
from django.contrib.contenttypes.fields import GenericRelation
class Product(models.Model):
...
moderation = GenericRelation(ItemModeration)
Then handle creation as you normally would with a related model, and filtering should work exactly as you stipulated. To work as your current system, you'd have to put in a hook or save method to create the related ItemModeration object when creating a new Product, but that's no different from other related django models. If you really want to keep the descriptor class, you can obviously make use of a secondary model field for the GenericRelation.
You can also add related_query_name to allow filtering the ItemModeration objects based only on the Product content type.
WARNING if you do use a GenericRelation note that it has a fixed cascading delete behavior. So if you don't want ItemModeration object to be deleted when you delete the Product, be careful to add a pre_delete hook or equivalent!
Update
I unintentionally ignored the OneToOne aspect of the question because the GenericForeignKey is a one-to-many relation, but similar functionality can be effected via smart use of QuerySets. It's true, you don't have access to product.moderation as a single object. But, for example, the following query iterates over a filtered list of products and extracts their name, the user's username, and the published date of the related ModerationItem:
Product.objects.filter(...).values_list(
'name', 'user__username', 'moderation__published'
)
You'll have to use the content_type to query the table by specific model type.
like this:
product_type = ContentType.objects.get_for_model(Product)
unpublished_products = ItemModeration.objects.filter(content_type__pk=product_type.id, published=False)
For more details on the topic check contenttypes doc
In Django, I have the following models.py
class Product(RandomPrimaryIdModel):
feature1 = models.CharField(max_length=20, blank=True, null=True)
feature2 = models.CharField(max_length=20, blank=True, null=True)
feature3 = models.CharField(max_length=20, blank=True, null=True)
class Mattress(Product):
category_type = models.CharField(max_length=50)
size = models.CharField(max_length=5)
def category(self):
return "bedding"
category = property(category)
I have the following views.py file
def update(request, id):
product = Product.objects.get(id=id)
...
In this method, update, can I call a method defined in the "Mattress" model from the Product model. For example, I want to write: if product.type == "mattress" where type has been defined in the Mattress Model and Mattress is a sub-model of Product.
Your example seems to sit between two different ways you can go, but is currently not correct. What is happening is that you are creating two tables: Product, and Mattress, and they are completely unrelated. Regardless of the fact that Mattress subclasses Product, it is just inheriting its structure. You cannot query anything in the Product table about a mattress because a mattress is in the Mattress table.
One way to go is to consider a Product just abstract, to be subclassed by actual products:
class Product(RandomPrimaryIdModel):
class Meta:
abstract=True
This will prevent a Product table from being created. Then you would directly query a mattress via: Mattress.objects.filter()
But this seems a bit limiting in terms of introducing many types of products, and having to manage different tables for them. The other way to go is to use a Product table, but use generic relations to support attaching any type of other table as a content object:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
class Product(RandomPrimaryIdModel):
feature1 = models.CharField(max_length=20, blank=True, null=True)
feature2 = models.CharField(max_length=20, blank=True, null=True)
feature3 = models.CharField(max_length=20, blank=True, null=True)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
With this, you would be able to set the content_object to be a Mattress instance. You can then use the ContentType to query:
p_type = ContentType.objects.get(name="mattress")
Product.objects.filter(content_type=p_type)
This looks like a case of automatic down casting. I needed a similar approach for a shopping cart that held generic 'ProductBase' instances but I needed to access the children's specific functions which were the actual products of type ProductDownloadable, ProductShipped, etc.
Django does not natively support this, but one could code it through introspection or use django-model-utils and once that is installed you could do:
# return a list 'child' classes of Product - in your case Mattresses
mattress_list = Product.objects.all().select_subclasses()
# return the direct 'child' class of Product - in your case Mattress class
mattress = Product.get_subclass(id=some_id) # returns the 'child' subclass
mattress.foo() # executes method on foo on Mattress class (not on Product class)