I designed a database for my django ecommerce project but it have some problems, the goal of the this design is to have products with different specifications for example a mobile cell has it's own properties and a television too, it is my models.py:
'''
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from django.shortcuts import reverse
from model_utils import FieldTracker
from . import uploaders
class Category(MPTTModel):
name = models.CharField(max_length=50, unique=True)
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True,
related_name='children')
slug = models.SlugField(max_length=75, unique=True)
tracker = FieldTracker(fields=['name'])
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
category_names = [self.name]
node = self
while node.parent:
node = node.parent
category_names.append(node.name)
return ' / '.join(category_names[::-1])
def get_absolute_url(self):
return reverse('product_by_category', args=(self.slug,))
class ProductType(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class ProductSpecifications(models.Model):
name = models.CharField(max_length=50)
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
related_name='specifications')
class Meta:
unique_together = ('name', 'product_type')
def __str__(self):
return self.name
class Product(models.Model):
name = models.CharField(max_length=100, unique=True)
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
related_name='products')
category = models.ForeignKey(Category, on_delete=models.CASCADE,
related_name='products')
price = models.PositiveBigIntegerField()
discount_price = models.PositiveBigIntegerField(null=True, blank=True)
description = models.TextField(null=True, blank=True)
image = models.ImageField(upload_to=uploaders.product_img_uploader)
slug = models.SlugField(max_length=150, unique=True)
tracker = FieldTracker(fields=['slug', 'name', 'product_type'])
def __str__(self):
return self.name
def set_discount(self, percentage):
self.discount_price = self.price * (1 - percentage)
self.save()
@property
def is_discounted(self):
return bool(self.discount_price)
def remove_discount(self):
self.discount_price = None
self.save()
class ProductSpecificationValue(models.Model):
specification = models.ForeignKey(ProductSpecifications, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE,
related_name='specifications')
value = models.CharField(max_length=75, null=True, blank=True)
def __str__(self):
return ''
class Meta:
unique_together = ('specification', 'product')
'''
And admin.py:
'''
from django.contrib import admin
from django.http import HttpResponseRedirect
from mptt.admin import MPTTModelAdmin
from .models import *
from .forms import ProductSpecForm
@admin.register(Category)
class CategoryAdmin(MPTTModelAdmin):
readonly_fields = ('slug',)
class SpecificationInline(admin.TabularInline):
model = ProductSpecifications
extra = 2
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
inlines = (SpecificationInline,)
class SpecificationValueInline(admin.TabularInline):
model = ProductSpecificationValue
# form = ProductSpecForm
# fields = ('specification', 'value')
# readonly_fields = ('specification',)
#
# def has_add_permission(self, request, obj):
# return False
#
# def has_delete_permission(self, request, obj=None):
# return False
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
inlines = (SpecificationValueInline,)
readonly_fields = ('slug',)
# def response_post_save_add(self, request, obj):
# return HttpResponseRedirect(
# reverse("admin:%s_%s_change" % (self.model._meta.app_label,
# self.model._meta.model_name), args=(obj.id,)))
'''
the problem is in product admin panel when you want to add or change a product, I want the select box for specification in SpecificationValueInline form show me only specifications related to the product type not all specifications in db, the lines that I commented in admin.py with some signals and a form was my approach to solve this issue i dont know if it was the best help me please!
signals.py:
'''
from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save
from .models import Category, Product, ProductSpecificationValue, ProductSpecifications
@receiver(pre_save, sender=Product)
@receiver(pre_save, sender=Category)
def initialize_slug(sender, instance, *args, **kwargs):
if (not instance.slug) or (instance.tracker.has_changed('name')):
instance.slug = instance.name.replace(' ', '_')
@receiver(post_save, sender=Product)
def initialize_specifications(sender, instance, created, **kwargs):
if created:
product_type = instance.product_type
for specification in product_type.specifications.all():
ProductSpecificationValue.objects.create(product=instance,
specification=specification)
elif instance.tracker.has_changed('product_type'):
ProductSpecificationValue.objects.filter(product=instance).delete()
product_type = instance.product_type
for specification in product_type.specifications.all():
ProductSpecificationValue.objects.create(product=instance,
specification=specification)
@receiver(post_save, sender=ProductSpecifications)
def add_new_specs_to_related_products(sender, instance, created, **kwargs):
if created:
product_type = instance.product_type
for product in product_type.products.all():
ProductSpecificationValue.objects.create(specification=instance,
product=product)
''' forms.py: '''
from django import forms
from django.forms import ModelChoiceField
from .models import ProductSpecificationValue, Product
class ProductSpecForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if hasattr(self.instance, 'product'):
self.fields['specification'] = ModelChoiceField(
queryset=self.instance.product.product_type.specifications.all())
class Meta:
model = ProductSpecificationValue
fields = ('specification', 'value')
'''
CodePudding user response:
you can use formfield_for_foreignkey
in SpecificationValueInline
class SpecificationValueInline(admin.TabularInline):
model = ProductSpecificationValue
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "specification":
product_id = request.resolver_match.kwargs.get('object_id')
productType = Product.objects.get(id = product_id).product_type
kwargs["queryset"] = ProductSpecification.objects.filter(product_type=productType)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
CodePudding user response:
mohsen ma answer was usefull I made some changes and it got better but I still doubt it it is enough or best practice, if user changes the product type he/she should stay on change page to fill the specification idk how: '''
@receiver(post_save, sender=Product)
def sync_specs_with_type(sender, instance, created, **kwargs):
if created or instance.tracker.has_changed('product_type'):
if not created:
instance.specifications.all().delete()
for spec in instance.product_type.specifications.all():
ProductSpecificationValue.objects.create(product=instance, specification=spec)
class SpecificationValueInline(admin.TabularInline):
model = ProductSpecificationValue
extra = 0
def formfield_for_foreignkey(self, db_field, request, **kwargs):
product_id = request.resolver_match.kwargs.get('object_id')
if product_id and db_field.name == "specification":
product_type = Product.objects.get(id=product_id).product_type
kwargs["queryset"] = ProductSpecifications.objects.filter(product_type=product_type)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
readonly_fields = ('slug',)
inlines = (SpecificationValueInline,)
def response_post_save_add(self, request, obj):
messages.add_message(request, messages.INFO, 'set you product specifications')
return HttpResponseRedirect(
reverse("admin:%s_%s_change" % (self.model._meta.app_label, self.model._meta.model_name), args=(obj.id,)))
def get_inlines(self, request, obj):
if obj:
return super().get_inlines(request, obj)
return ()
'''