awesto / django-shop

A Django based shop system
http://www.django-shop.org
BSD 3-Clause "New" or "Revised" License
3.21k stars 1.04k forks source link

How I can use different filters for different categories? #571

Open vivazzi opened 7 years ago

vivazzi commented 7 years ago

For example, we have two different intities:

class Product(CMSPageReferenceMixin, BaseProduct):
    ...

class TyreModel(Product):
    brand = models.ForeignKey(TyreBrand)

    SUMMER = 'summer'
    WINTER = 'winter'
    ALL_WEATHER = 'all_weather'
    SEASONS = ((SUMMER, 'summer'), (WINTER, 'winter'), (ALL_WEATHER, 'all_weather'))
    season = models.CharField('season', max_length=12, choices=SEASONS)

@python_2_unicode_compatible
class DiskModel(Product, PicModel):
    brand = models.ForeignKey(DiskBrand)

    STAMP = 'stamp'
    CAST = 'cast'
    FORGED = 'forged'
    DISK_TYPES = ((STAMP, 'stamp'), (CAST, 'cast'), (FORGED, 'forged'))
    disk_type = models.CharField('disk type', max_length=6, choices=DISK_TYPES)

Note, we have brand field with different ForeignKey (TyreBrand and DiskBrand) in models, so we need consider this moment.

My FilterSet shouldn't be common. It's different for Tyre page and Disk page.

As I understand, I need use something like this:

class TyreFilterSet(django_filters.FilterSet):
    brand = django_filters.ModelChoiceFilter(label='Brand', name='tyremodel__brand', queryset=TyreBrand.objects.all(),
                                             empty_label='Любой', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    season = django_filters.ChoiceFilter(label='season', name='tyremodel__season', choices=TyreModel.SEASONS,
                                         empty_label='any', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    class Meta:
        model = Product
        form = FilterForm
        fields = ()

    @classmethod
    def get_render_context(cls, request, queryset):
        # create filter set with bound form, to enable the selected option
        filter_set = cls(data=request.GET)

        # we only want to show manufacturers for products available in the current list view
        filter_field = filter_set.filters['brand'].field
        filter_field.queryset = filter_field.queryset.filter(id__in=queryset.values_list('tyremodel__brand_id'))

        return dict(filter_set=filter_set)

class DiskFilterSet(django_filters.FilterSet):
    brand = django_filters.ModelChoiceFilter(label='Производитель', name='diskmodel__brand', queryset=DiskBrand.objects.all(),
                                             empty_label='Любой', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    disk_type = django_filters.ChoiceFilter(label='Тип', name='diskmodel__disk_type', choices=DiskModel.DISK_TYPES,
                                            empty_label='Любой', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    class Meta:
        model = Product
        form = FilterForm
        fields = ()

    @classmethod
    def get_render_context(cls, request, queryset):
        # create filter set with bound form, to enable the selected option
        filter_set = cls(data=request.GET)

        # we only want to show manufacturers for products available in the current list view
        filter_field = filter_set.filters['brand'].field
        filter_field.queryset = filter_field.queryset.filter(id__in=queryset.values_list('diskmodel__brand_id'))

        return dict(filter_set=filter_set)

And how I can use TyreFilterSet or DiskFilterSet according to Tyre and Disk pages? Now, in urls I have:

urlpatterns = [
    url(r'^$', CustomCMSPageCatalogWrapper.as_view(
        search_serializer_class=CatalogSearchSerializer,
        filter_class=TyreFilterSet,
        limit_choices_to=Q(active=True),
    )),
    ...
]

And how use corresponding templates? Now I use:

{% load static sekizai_tags %}

{% addtoblock "js" %}<script src="{% static 'shop/js/filter-form.js' %}"></script>{% endaddtoblock %}
{% addtoblock "ng-requires" %}django.shop.filter{% endaddtoblock %}

<form shop-product-filter="['brand', 'season']" style="margin-bottom: 10px;">
    {{ filter.filter_set.form.as_div }}
</form>

But It is for tyre only.

In Ideal, I think we should use someone plugin, say, FilterPlugin(CMSPlugin), in which we can select nessesary FilterSet (Ex., TyreFilterSet or DiskFilterSet). And plugin will be use FilterSet and corresponding template. This options we can add to settings.py like that:

FILTERS = (('TyreFitlerSet', 'myshop/catalog/tyre_filter.html), 
      ('DiskFitlerSet', 'myshop/catalog/disk_filter.html))

We should like can use pool of filters and add TyreFilterSet and DiskFilterSet to pool.

Also, I can suggest add variable to filter.html to no need recount our fileds, for example, instead of:

<form shop-product-filter="['brand', 'season']" style="margin-bottom: 10px;">
    {{ filter.filter_set.form.as_div }}
</form>

We should like have:

<form shop-product-filter="{{ fileds }}" style="margin-bottom: 10px;">
    {{ filter.filter_set.form.as_div }}
</form>

where fields is list (['brand', 'season']) which we declare in FilterSet in Meta class (fields):

class TyreFilterSet(django_filters.FilterSet):
    brand = django_filters.ModelChoiceFilter(...)
    season = django_filters.ChoiceFilter(...)

    class Meta:
        model = Product
        form = FilterForm
        fields = ('brand', 'season')

How about this idea?

vivazzi commented 7 years ago

I find temporary solution as work around and it works very well! Idea of this solution is using of common FilterSet and using concrete filter fields in corresponing templates. Look at my case:

filters.py:


class CommonFilterSet(django_filters.FilterSet):
    has_in_stock = django_filters.BooleanFilter(label='has in stock?', help_text='',
                                                widget=NullBooleanSelect(attrs={'ng-change': 'filterChanged()'}))

class TyreFilterSet(CommonFilterSet):
    tyre_brand = django_filters.ModelChoiceFilter(label='brand', name='tyremodel__brand', queryset=TyreBrand.objects.all(),
                                                  empty_label='Any', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    tyre_season = django_filters.ChoiceFilter(label='season', name='tyremodel__season', choices=TyreModel.SEASONS,
                                              empty_label='Any', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

class DiskFilterSet(CommonFilterSet):
    disk_brand = django_filters.ModelChoiceFilter(label='brand', name='diskmodel__brand', queryset=DiskBrand.objects.all(),
                                                  empty_label='Any', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

    disk_type = django_filters.ChoiceFilter(label='type', name='diskmodel__disk__disk_type', choices=DiskModel.DISK_TYPES,
                                            empty_label='Any', help_text='', widget=Select(attrs={'ng-change': 'filterChanged()'}))

class CustomFilterSet(DiskFilterSet, TyreFilterSet):

    class Meta:
        model = Product
        form = FilterForm
        fields = ('has_in_stock', 'is_pu')

    @classmethod
    def get_render_context(cls, request, queryset):
        filter_set = cls(data=request.GET)

        filter_field = filter_set.filters['tyre_brand'].field
        filter_field.queryset = filter_field.queryset.filter(id__in=queryset.values_list('tyremodel__brand_id'))

        filter_field = filter_set.filters['disk_brand'].field
        filter_field.queryset = filter_field.queryset.filter(id__in=queryset.values_list('diskmodel__brand_id'))

        return dict(filter_set=filter_set)

After, I use CustomFilterSet in urls.py:

from myshop.filters import CustomFilterSet

urlpatterns = [
    url(r'^$', CMSPageCatalogWrapper.as_view(
        search_serializer_class=CatalogSearchSerializer,
        filter_class=CustomFilterSet,
    )),
    ...
]

After, I add custom template_filter:

@register.simple_tag
def get_filter_fields(form, filter_type):
    types = {'tyre': ('tyre_brand', 'tyre_season', 'has_in_stock'),
             'disk': ('disk_brand', 'disk_type', 'has_in_stock')}

    l = types[filter_type]
    return {'fields': [form[f] for f in l], 'fields_str': json.dumps(l)}

And I add templates: filter.html:

{% load static sekizai_tags myshop_tags %}

{% addtoblock "js" %}<script src="{% static 'shop/js/filter-form.js' %}"></script>{% endaddtoblock %}
{% addtoblock "ng-requires" %}django.shop.filter{% endaddtoblock %}

{% block filter_content %}{% endblock %}

tyre_filter.html:

{% extends 'myshop/catalog/filter.html' %}
{% load myshop_tags %}

{% block filter_content %}
    {% get_filter_fields filter.filter_set.form 'tyre' as data %}

    <form shop-product-filter="{{ data.fields_str }}">
        {% for field in data.fields %}
            <p>{{ field.label }} {{ field }}</p>
        {% endfor %}
    </form>
{% endblock %}

disk_filter.html:

{% extends 'myshop/catalog/filter.html' %}
{% load myshop_tags %}

{% block filter_content %}
    {% get_filter_fields filter.filter_set.form 'disk' as data %}

    <form shop-product-filter="{{ data.fields_str }}">
        {% for field in data.fields %}
            <p>{{ field.label }} {{ field }}</p>
        {% endfor %}
    </form>
{% endblock %}

Finally, I add options to settings.py:

CMSPLUGIN_CASCADE = {
    ...
    'plugins_with_extra_render_templates': {
        'CustomSnippetPlugin': [
            ('shop/catalog/product-heading.html', _("Product Heading")),
            ('myshop/catalog/tyre_filter.html', 'tyre filter'),
            ('myshop/catalog/disk_filter.html', 'disk filter'),
        ],
    },
    ...
}

That's all. I think it will be useful for someone. Please, fix me if something is wrong.

jrief commented 7 years ago

Without having looked at it in detail, I would have chosen a similar solution:

CustomFilterSet. get_render_context(...) would return a dict with two different filter_sets, say return dict(filter_set_tyre=filter_set_tyre, filter_set_disk=filter_set_disk). Then the two different snippet templates would just reference either the filter_set_tyre or the filter_set_disk.

Not sure if my solution would work, but that would be my first attempt.

btw, great question anyway!

vivazzi commented 7 years ago

Your solution looks like clearer but I try to implement your solution and I meet some difficuts. If we want to use filter_set_tyre and filter_set_disk, we need use something like:

    def get_render_context(cls, request, queryset):
        filter_set_tyre = cls(data=request.GET)
        filter_set_disk = cls(data=request.GET)

        ...

        return dict(filter_set_tyre=filter_set_tyre, filter_set_disk=filter_set_disk)

And we need clean filter_set_tyre from disk filter fields and same for filter_set_disk.

And even if we do this, the problem of filter management will remain, because I think this should be decide in something plugin which saves necessary template and FilterSet (In preset settings, ex., in settings.py).

I tried to realize this idea, but It turned out to be difficult for me.

markusmo commented 6 years ago

Thanks for the Input. I recently had the same requirement.

Only one problem for me: My dropdown menus only show "All ..." and no queryset entries. Does anybody know, what may be the issue ...?

markusmo commented 6 years ago

Have been able to fix my problem. I had to resolve my foreign keys properly :-)