Home > database >  django ecommerce product db design
django ecommerce product db design

Time:11-04

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 ()

'''

  • Related