Search code examples
pythondjangodjango-modelsdjango-admindjango-admin-tools

Search with parent foreignkey, grandparent foreignkey in django-admin


I wanna be able to search a product with the category that isnt directly linked to it but the category is grandfather or great_great grandfather and so on above on the category tree of the category that is actually linked to the product itself. The only way i could implement was with the following which is ofcourse not scalable and proper way to do it:

 def get_queryset(self, request):
        queryset = super().get_queryset(request).select_related('brand', 'category', 'category__parent')
        category_name = request.GET.get('category__name', None)

        if category_name:
            # Filter the queryset to include products with the specified category or its ancestors
            queryset = queryset.filter(
                Q(category__name=category_name) |
                Q(category__parent__name=category_name) |
                Q(category__parent__parent__name=category_name) |
                # Add more levels as needed based on the depth of your category tree
            )

        return queryset

And here are the code as of now: Product``models.py

class Product(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(blank=True, null=True)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    image = models.ImageField(upload_to='product_images/')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL,related_name='product_category', null=True, blank=True)
    brand = models.ForeignKey(Brand, on_delete=models.SET_NULL, related_name='product_brand', null=True, blank=True)
    tags = GenericRelation(TaggedItem, related_query_name='product')
    ratings = GenericRelation(Rating, related_query_name='product')
    active = models.BooleanField(default=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    state = models.CharField(max_length=2, choices=PublishStateOptions.choices, default=PublishStateOptions.DRAFT)
    publish_timestamp = models.DateTimeField(
        # means that the publish_timestamp field will not automatically be set to the current date and time when a new instance of this model is created.
        auto_now_add=False,
        # means that the publish_timestamp field will not automatically be updated to the current date and time every time the model is saved.
        auto_now=False,
        blank=True,
        null=True
    )

    def __str__(self):
        return self.name

Category``models.py

class Category(models.Model):
    name = models.CharField(max_length=255)
    parent = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='subcategories', null=True, blank=True)

    class Meta:
        verbose_name_plural = 'Categories'

    def __str__(self):
        return self.name

Product``admin.py

class ProductAdmin(admin.ModelAdmin):
    list_display = ['name', 'price', 'brand', 'category']
    inlines = [TaggedItemInline]
    list_filter = [
        'brand__name',
        'category__name' 
    ]
    search_fields = ['name', 'price', 'brand__name', 'category__name', 'category__parent__name']

    def get_queryset(self, request):
        return super().get_queryset(request).select_related('brand', 'category', 'category__parent')

    def get_brand_name(self, obj):
        return obj.brand.name if obj.brand else None
    
    def get_category_name(self, obj):
        return obj.category.name if obj.category else None

    get_brand_name.short_description = 'Brand'
    get_category_name.short_description = 'Category'

    # Define the labels for list filters
    list_filter = (
        ('brand', admin.RelatedOnlyFieldListFilter),
        ('category', admin.RelatedOnlyFieldListFilter),
    )

admin.site.register(Product, ProductAdmin)

Solution

  • After an hour, I figured it out.

    def get_search_results(self, request, queryset, search_term):
        queryset, use_distinct = super().get_search_results(request, queryset, search_term)
    
        # Search for products based on ascendant category names recursively
        matching_categories = self.find_matching_categories(search_term)
        ascendant_categories = self.get_all_ascendant_categories(matching_categories)
        products_with_ascendant_categories = self.model.objects.filter(category__in=ascendant_categories)
        queryset |= products_with_ascendant_categories
    
        return queryset, use_distinct
    
    def find_matching_categories(self, search_term):
        # Find categories that match the search term
        return Category.objects.filter(name__icontains=search_term)
    
    def get_all_ascendant_categories(self, categories):
        # Recursively retrieve all ascendant categories
        ascendant_categories = list(categories)
        new_ascendants = Category.objects.filter(parent__in=categories)
        if new_ascendants:
            ascendant_categories.extend(self.get_all_ascendant_categories(new_ascendants))
        return ascendant_categories
    

    When users search for products, they can now use the names of the product's category's ascendants. The get_search_results() method is customized to achieve this. When a user searches, the code identifies categories that match the search term and then recursively collects all ancestors of these categories. This ensures that products from categories and their ascendants are included in the search results.