diff --git a/products/admin.py b/products/admin.py index c2fff0d..f2f78d7 100644 --- a/products/admin.py +++ b/products/admin.py @@ -1,12 +1,20 @@ from django.contrib import admin +from django.forms import ALL_FIELDS +from django.utils.translation import ugettext_lazy as _ + from import_export import resources, fields, widgets from import_export.admin import ImportExportModelAdmin +from jet.admin import CompactInline from rangefilter.filter import DateRangeFilter, DateTimeRangeFilter -from .models import Product, ProductCategory, ProductImage, ProductAttribute, ProductAttributeValue, Manufacturer +from cart.admin import ProductOfferInlineAdmin +from core.admin import SafeModelAdmin +from .forms import ProductAdminForm +from .models import ( + Product, ProductCategory, ProductImage, Manufacturer +) -# class CustomModelResource(resources.ModelResource): def before_import_row(self, row, **kwargs): """ @@ -27,77 +35,45 @@ class CustomManyToManyWidget(widgets.ManyToManyWidget): return self.model.objects.get(name=t1) if t1 else None -# -# -# # class CustomForeignKeyWidget(widgets.ForeignKeyWidget): -# # def clean(self, value, row=None, *args, **kwargs): -# # return self.model.objects.get_or_create(name=value)[0] -# class ProductImageInline(admin.TabularInline): model = ProductImage extra = 0 -# -# # class ProductAttributeInline(admin.TabularInline): -# # model = ProductAttribute -# # extra = 1 -# # verbose_name_plural = 'ProductAttribute' -# # suit_classes = 'suit-tab suit-tab-PA' -# # -class AttributeChoiceValueInline(admin.TabularInline): - model = ProductAttributeValue - # prepopulated_fields = {'slug': ('name',)} - extra = 1 - verbose_name_plural = 'AttributeChoiceValue' - suit_classes = 'suit-tab suit-tab-ACV' +# class AttributeValueInline(admin.TabularInline): +# model = ProductAttributeValue +# form = ProductAttributeValueAdminForm +# extra = 1 +# verbose_name_plural = _('Значение аттрибута') +# can_delete = True -# # -# # class OfferInline(admin.TabularInline): -# # model = Offer -# # extra = 1 -# # verbose_name_plural = 'Offers' -# # suit_classes = 'suit-tab suit-tab-offers' -# @admin.register(ProductCategory) -class ProductCategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'slug') +class ProductCategoryAdmin(SafeModelAdmin): + list_display = ('name', 'slug', 'parent', 'status') search_fields = ('name', 'slug') - list_filter = ('status',('create_at',DateRangeFilter),('updated_at', DateTimeRangeFilter)) - + list_filter = ('status', ('create_at', DateRangeFilter), ('updated_at', DateTimeRangeFilter)) -# # class AttributeChoiceValueAdmin(admin.ModelAdmin): -# # list_display = [field.name for field in ProductCategory._meta.fields] -# # -# # class Meta: -# # model = AttributeChoiceValue -# # -# # admin.site.register(AttributeChoiceValue, AttributeChoiceValueAdmin) -@admin.register(ProductAttribute) -class ProductAttributeAdmin(admin.ModelAdmin): - list_display = ('slug','name') - search_fields = ('slug','name') - inlines = [AttributeChoiceValueInline] - # prepopulated_fields = {'slug': ('name',)} - - suit_form_tabs = (('general', 'General'), - ('ACV', 'AttributeValues'),) - - class Meta: - model = ProductAttribute +# @admin.register(ProductAttribute) +# class ProductAttributeAdmin(ImportExportModelAdmin, SafeModelAdmin): +# inlines = [ +# # AttributeValueInline +# ] +# list_display = ('name', 'slug') +# search_fields = ('name', 'slug') @admin.register(Manufacturer) -class ManufacturerAdmin(admin.ModelAdmin): - list_display = ('slug', 'name') - search_fields = ('slug', 'name'), +class ManufacturerAdmin(ImportExportModelAdmin, SafeModelAdmin): + list_display = ('name', 'slug', 'status') + search_fields = ('name', 'slug',) + list_filter = ('status',) class Meta: model = Manufacturer -# -# + + class ProductResource(CustomModelResource): # id = fields.Field(default=generate_Jid(prefix='J'), # readonly=True, @@ -108,52 +84,31 @@ class ProductResource(CustomModelResource): default=None, widget=widgets.CharWidget(), ) - # price = fields.Field(column_name='price', attribute='price', - # default=0, - # widget=widgets.DecimalWidget(), - # ) description = fields.Field(column_name='description', attribute='description', default=None, widget=widgets.CharWidget(), ) - # producer = fields.Field(column_name='producer', attribute='producer', - # default=None, - # widget=widgets.CharWidget(), - # ) - - category = fields.Field(column_name='category', attribute='category', + category = fields.Field(column_name='category', attribute='parent', default=None, - widget=widgets.ForeignKeyWidget(ProductCategory, field='name'), - ) - producer = fields.Field(column_name='producer', attribute='producer', + widget=widgets.ForeignKeyWidget(ProductCategory, field='name')) + + producer = fields.Field(column_name='producer', + attribute='producer', default=None, - widget=widgets.ForeignKeyWidget(Manufacturer, field='name'), - ) - attributes = fields.Field(column_name='attributes', attribute='attributes', - default=None, - widget=CustomManyToManyWidget(ProductAttribute, field="name"), - ) - is_active = fields.Field(column_name='is_active', attribute='is_active', - default=1, - widget=widgets.BooleanWidget()) - - discount_policy = fields.Field(column_name='discount_policy', attribute='discount_policy', + widget=widgets.ForeignKeyWidget(Manufacturer, field='name')) + + discount_policy = fields.Field(column_name='discount_policy', + attribute='discount_policy', default={}, widget=widgets.CharWidget()) - # delete = fields.Field(column_name='delete', attribute='delete', - # default=0, - # widget=widgets.BooleanWidget()) - - # def for_delete(self, row, instance): - # return self.fields['delete'].clean(row) class Meta: model = Product - fields = ('id', 'name', 'description', 'producer', 'category', 'is_active', 'attributes', 'discount_policy') + fields = ('id', 'name', 'description', 'producer', 'parent', 'is_active', 'attributes', 'discount_policy') export_order = ( - 'id', 'name', 'producer', 'is_active', 'category', 'attributes', 'description', 'discount_policy') + 'id', 'name', 'producer', 'is_active', 'parent', 'attributes', 'description', 'discount_policy') # import_id_fields = ('name',) def dehydrate_str_choices(self, obj): @@ -161,53 +116,23 @@ class ProductResource(CustomModelResource): return obj.str_choices() -# -@admin.register(Product) -class ProductAdmin(ImportExportModelAdmin): - list_display = ['id', 'name', 'category', 'manufacturer', 'status'] - list_filter = ['status', 'create_at', 'updated_at', 'category'] - search_fields = ['name', 'id'] - - resource_class = ProductResource - - -# class OfferResource(CustomModelResource): -# name = fields.Field(column_name='name', attribute='name', -# default=None, -# widget=widgets.CharWidget(), -# ) -# -# price = fields.Field(column_name='price', attribute='price', -# default=0, -# widget=widgets.DecimalWidget(), -# ) -# -# products = fields.Field(column_name='products', attribute='products', -# widget=widgets.ForeignKeyWidget(Product, field='name'), -# ) -# -# is_active = fields.Field(column_name='is_active', attribute='is_active', -# default=1, -# widget=widgets.BooleanWidget()) -# -# attributes = fields.Field(column_name='attributes', attribute='attributes', -# default={}, -# widget=widgets.CharWidget()) -# -# class Meta: -# model = Offer -# fields = ('name', 'products', 'price', 'is_active', 'attributes') -# export_order = ('name', 'products', 'attributes', 'is_active', 'price') -# import_id_fields = ('name',) - -# class OfferAdmin(ImportExportModelAdmin): -# list_display = ['id', 'name', 'products', 'price', 'is_active', 'attributes'] -# resource_class = OfferResource - -@admin.register(ProductImage) -class ProductImageAdmin(admin.ModelAdmin): - list_display = [field.name for field in ProductImage._meta.fields] +class ProductImageInlineAdmin(CompactInline): + model = ProductImage + exclude = ('filename',) + extra = 1 + show_change_link = True + can_delete = True - class Meta: - model = ProductImage +@admin.register(Product) +class ProductAdmin(ImportExportModelAdmin, SafeModelAdmin): + inlines = ( + ProductImageInlineAdmin, + ProductOfferInlineAdmin, + ) + list_display = ('name', 'parent', 'manufacturer', 'status') + list_filter = ('status', 'create_at', 'updated_at', 'parent') + search_fields = ('name', 'id',) + readonly_fields = ('slug',) + resource_class = ProductResource + form = ProductAdminForm diff --git a/products/context_processors.py b/products/context_processors.py index 79e2b0a..b032388 100644 --- a/products/context_processors.py +++ b/products/context_processors.py @@ -1,16 +1,25 @@ from django.urls import reverse_lazy -from products.forms import ProductSearchForm -from products.models import ProductCategory +from cart.forms import ProductOfferPriceFilterForm, ProductOfferSupplyTypeFilterForm, ProductOfferSupplyTargetFilterForm +from products.forms import ( + ProductSearchForm, ProductManufacturerFilterForm +) +from products.models import ProductCategory, Manufacturer, Product def product_search_form(request): - #@TODO: APPLY SEARCH IN THE CONTEXT OF CHOSSEN DIRECTORY AND SET OF FILTERS - left_product_search_form = ProductSearchForm(submit_css_class='left-menu__search-btn') - content_product_search_form = ProductSearchForm(submit_css_class='content__search-btn') + # @TODO: APPLY SEARCH IN THE CONTEXT OF SET OF FILTERS + if not "products" in request.resolver_match.view_name: + product_form_action = {'viewname': 'products:product_list', 'kwargs': {}} + else: + product_form_action = {'viewname': request.resolver_match.view_name, 'kwargs': request.resolver_match.kwargs} + left_product_search_form = ProductSearchForm(product_form_action=product_form_action, + submit_css_class='left-menu__search-btn') + content_product_search_form = ProductSearchForm(product_form_action=product_form_action, + submit_css_class='content__search-btn') if ProductSearchForm.form_action in request.resolver_match.view_name: if request.resolver_match.kwargs.get('category_slug'): - product_kwargs= request.resolver_match.kwargs + product_kwargs = request.resolver_match.kwargs product_list_in_cat = reverse_lazy(ProductSearchForm.form_action, kwargs=product_kwargs) left_product_search_form.helper.form_action = product_list_in_cat content_product_search_form.helper.form_action = product_list_in_cat @@ -19,17 +28,82 @@ def product_search_form(request): left_product_search_form.initial = initial_data content_product_search_form.initial = initial_data - return { 'left_product_search_form': left_product_search_form, 'content_product_search_form': content_product_search_form } -def product_root_categories(request): - current_category = request.resolver_match.kwargs.get('category_slug',None) - current_category = ProductCategory.objects.filter(slug=current_category).first() +def product_fitler_formset(request): + if not "products" in request.resolver_match.view_name: + product_form_action = {'viewname': 'products:product_list', 'kwarg': {}} + else: + product_form_action = {'viewname': request.resolver_match.view_name, 'kwargs': request.resolver_match.kwargs} + + return { + 'left_product_filter_formset':{ + 'manufacturer': ProductManufacturerFilterForm( + product_form_action=product_form_action, + query_params=request.GET + ), + 'price': ProductOfferPriceFilterForm( + product_form_action=product_form_action, + query_params=request.GET + ), + 'supply_type': ProductOfferSupplyTypeFilterForm( + product_form_action=product_form_action, + query_params=request.GET + ), + 'supply_target': ProductOfferSupplyTargetFilterForm( + product_form_action=product_form_action, + query_params=request.GET + ) + } + } + + +def product_manufacture_list(request): + man_qs = Manufacturer.active + if request.resolver_match.kwargs.get('category_slug'): + cat_qs = ProductCategory.objects.filter(slug__exact=request.resolver_match.kwargs.get('category_slug')) + prod_qs = Product.active.filter(category__in=cat_qs.get_descendants(include_self=True)) + man_qs = man_qs.filter(pk__in=prod_qs.distinct('manufacturer__pk').all()) + return {'manufacturer_list': man_qs.all()} + + +def product_categories(request): + current_category_path = request.resolver_match.kwargs.get('path', '') + current_category = None + try: + current_category_slug = current_category_path.split('/')[-2] # slug of the instance + except IndexError: + current_category_slug = None + + if ProductCategory.active.filter(slug__exact=current_category_slug).exists(): + current_category = ProductCategory.objects.filter(slug=current_category_slug).first() + + if current_category: + descendant_categories = current_category.get_descendants() + else: + descendant_categories = ProductCategory.active.root_nodes() + + return { + 'hasCategories': len(descendant_categories) > 0 or current_category, + 'product_categories': descendant_categories, + 'the_product_category': current_category + } + + +def product_detail(request): + product_path = request.resolver_match.kwargs.get('path', '') + + try: + product_slug = product_path.split('/')[-2] # slug of the instance + except IndexError: + product_slug = None + + product = Product.active.filter(slug__exact=product_slug).first() + return { - 'product_categories': ProductCategory.active.get_categories(current_category), - 'the_product_category' : current_category + 'the_product': product } diff --git a/products/fixtures/category.json b/products/fixtures/category.json new file mode 100644 index 0000000..df2a6e7 --- /dev/null +++ b/products/fixtures/category.json @@ -0,0 +1,70 @@ +[ + { + "model": "products.productcategory", + "pk": 1, + "fields": { + "create_at": "2018-08-12T19:23:11.150Z", + "updated_at": "2018-08-12T19:23:33.159Z", + "name": "Microsoft", + "slug": "windows", + "parent": null, + "image": "products/windows/windows.svg", + "status": 25, + "lft": 1, + "rght": 2, + "tree_id": 1, + "level": 0 + } + }, + { + "model": "products.productcategory", + "pk": 2, + "fields": { + "create_at": "2018-08-12T19:23:49.581Z", + "updated_at": "2018-08-12T19:23:49.581Z", + "name": "\u0410\u043d\u0442\u0438\u0432\u0438\u0440\u0443\u0441", + "slug": "antivirus", + "parent": null, + "image": "products/antivirus/antivirus_oDon9N3.svg", + "status": 25, + "lft": 1, + "rght": 2, + "tree_id": 2, + "level": 0 + } + }, + { + "model": "products.productcategory", + "pk": 3, + "fields": { + "create_at": "2018-08-12T19:23:59.394Z", + "updated_at": "2018-08-12T19:23:59.394Z", + "name": "\u0412\u0438\u0440\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f", + "slug": "virtualizatsiya", + "parent": null, + "image": "products/virtualizatsiya/virtualization_tEnq2Bu.svg", + "status": 25, + "lft": 1, + "rght": 4, + "tree_id": 3, + "level": 0 + } + }, + { + "model": "products.productcategory", + "pk": 4, + "fields": { + "create_at": "2018-08-12T20:05:06.726Z", + "updated_at": "2018-08-12T20:05:06.726Z", + "name": "Oracle", + "slug": "oracle", + "parent": 3, + "image": "", + "status": 25, + "lft": 2, + "rght": 3, + "tree_id": 3, + "level": 1 + } + } +] diff --git a/products/fixtures/manufacturer.json b/products/fixtures/manufacturer.json new file mode 100644 index 0000000..3aeebe7 --- /dev/null +++ b/products/fixtures/manufacturer.json @@ -0,0 +1 @@ +[{"model": "products.manufacturer", "pk": 1, "fields": {"create_at": "2018-08-12T19:24:12.126Z", "updated_at": "2018-08-12T19:24:12.126Z", "name": "Microsoft", "slug": "microsoft", "image": "", "status": 25}}, {"model": "products.manufacturer", "pk": 2, "fields": {"create_at": "2018-08-12T19:24:18.725Z", "updated_at": "2018-08-12T19:24:18.725Z", "name": "Parallels", "slug": "parallels", "image": "", "status": 25}}, {"model": "products.manufacturer", "pk": 3, "fields": {"create_at": "2018-08-12T19:24:31.655Z", "updated_at": "2018-08-12T19:24:31.655Z", "name": "Eset", "slug": "eset", "image": "", "status": 25}}] \ No newline at end of file diff --git a/products/fixtures/products.json b/products/fixtures/products.json deleted file mode 100644 index 2e21761..0000000 --- a/products/fixtures/products.json +++ /dev/null @@ -1,533 +0,0 @@ -[ - { - "model": "products.productcategory", - "pk": 1, - "fields": { - "name": "Information security", - "slug": "information-security", - "is_active": true, - "parent": null, - "lft": 1, - "rght": 6, - "tree_id": 2, - "level": 0 - } -}, -{ - "model": "products.productcategory", - "pk": 2, - "fields": { - "name": "Document manipulation", - "slug": "document-manipulation", - "is_active": true, - "parent": null, - "lft": 1, - "rght": 4, - "tree_id": 1, - "level": 0 - } -}, -{ - "model": "products.productcategory", - "pk": 3, - "fields": { - "name": "System software", - "slug": "system-software", - "is_active": true, - "parent": null, - "lft": 1, - "rght": 6, - "tree_id": 3, - "level": 0 - } -}, -{ - "model": "products.productcategory", - "pk": 4, - "fields": { - "name": "Anti-virus software", - "slug": "anti-virus-software", - "is_active": true, - "parent": 1, - "lft": 2, - "rght": 3, - "tree_id": 2, - "level": 1 - } -}, -{ - "model": "products.productcategory", - "pk": 5, - "fields": { - "name": "Data recovery", - "slug": "data-recovery", - "is_active": true, - "parent": 1, - "lft": 4, - "rght": 5, - "tree_id": 2, - "level": 1 - } -}, -{ - "model": "products.productcategory", - "pk": 6, - "fields": { - "name": "OS", - "slug": "os", - "is_active": true, - "parent": 3, - "lft": 4, - "rght": 5, - "tree_id": 3, - "level": 1 - } -}, -{ - "model": "products.productcategory", - "pk": 7, - "fields": { - "name": "DB", - "slug": "db", - "is_active": true, - "parent": 3, - "lft": 2, - "rght": 3, - "tree_id": 3, - "level": 1 - } -}, -{ - "model": "products.productcategory", - "pk": 8, - "fields": { - "name": "Microsoft Office", - "slug": "microsoft-office", - "is_active": true, - "parent": 2, - "lft": 2, - "rght": 3, - "tree_id": 1, - "level": 1 - } -}, -{ - "model": "products.productattribute", - "pk": 1, - "fields": { - "name": "License type", - "slug": "license-type" - } -}, -{ - "model": "products.productattribute", - "pk": 2, - "fields": { - "name": "License term", - "slug": "license-term" - } -}, -{ - "model": "products.productattribute", - "pk": 3, - "fields": { - "name": "Technical support", - "slug": "technical-support" - } -}, -{ - "model": "products.productattribute", - "pk": 4, - "fields": { - "name": "Number users", - "slug": "number-users" - } -}, -{ - "model": "products.productattribute", - "pk": 5, - "fields": { - "name": "Type of organization", - "slug": "type-organization" - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 1, - "fields": { - "name": "New", - "slug": "new", - "attribute": 1 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 2, - "fields": { - "name": "Prolongation", - "slug": "prolongation", - "attribute": 1 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 3, - "fields": { - "name": "Migration", - "slug": "migration", - "attribute": 1 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 4, - "fields": { - "name": "One year", - "slug": "one-year", - "attribute": 2 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 5, - "fields": { - "name": "Two years", - "slug": "two-years", - "attribute": 2 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 6, - "fields": { - "name": "Standard AAS", - "slug": "standard-aas", - "attribute": 3 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 7, - "fields": { - "name": "Extended AAP", - "slug": "extended-aap", - "attribute": 3 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 8, - "fields": { - "name": "from 1 to 49", - "slug": "1-49", - "attribute": 4 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 9, - "fields": { - "name": "from 50 to 99", - "slug": "50-99", - "attribute": 4 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 10, - "fields": { - "name": "from 100 to 299", - "slug": "100-299", - "attribute": 4 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 11, - "fields": { - "name": "Version Upgrade", - "slug": "version-upgrade", - "attribute": 1 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 12, - "fields": { - "name": "commercial", - "slug": "commercial", - "attribute": 5 - } -}, -{ - "model": "products.attributechoicevalue", - "pk": 13, - "fields": { - "name": "educational", - "slug": "educational", - "attribute": 5 - } -}, -{ - "model": "products.productclass", - "pk": 1, - "fields": { - "name": "Antivirus software class", - "has_variants": true, - "variant_attributes": [ - 2, - 1, - 5 - ] - } -}, -{ - "model": "products.productclass", - "pk": 2, - "fields": { - "name": "Data recovery class", - "has_variants": true, - "variant_attributes": [ - 1, - 3 - ] - } -}, -{ - "model": "products.productclass", - "pk": 3, - "fields": { - "name": "Document manipulation class", - "has_variants": true, - "variant_attributes": [] - } -}, -{ - "model": "products.productclass", - "pk": 4, - "fields": { - "name": "DB class", - "has_variants": true, - "variant_attributes": [ - 2, - 4 - ] - } -}, -{ - "model": "products.productclass", - "pk": 5, - "fields": { - "name": "OS class", - "has_variants": true, - "variant_attributes": [ - 5 - ] - } -}, -{ - "model": "products.product", - "pk": 85, - "fields": { - "name": "Kaspersky Endpoint Security \u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430 CLOUD", - "slug": "kaspersky-endpoint-security-dlya-biznesa-cloud", - "price": "1730.00", - "points": "173.00", - "description": "Kaspersky Endpoint Security Cloud \u2013 \u044d\u0442\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u044f\u043c \u043c\u0430\u043b\u043e\u0433\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u0438 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0434\u0435\u0436\u043d\u0443\u044e \u0437\u0430\u0449\u0438\u0442\u0443 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440\u043e\u0432, \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432 \u0438\u0437 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0439 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f. \u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u043e\u043a\u0443\u043f\u043a\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0441 \u043b\u044e\u0431\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u043a \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", - "short_description": "", - "producer": "Kaspersky", - "image": "products/2017/05/26/thumb_1473758588.png", - "discount": 0, - "stock": 10, - "category": 4, - "product_class": 1, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 86, - "fields": { - "name": "ESET NOD32 Antivirus Business Edition", - "slug": "eset-nod32-antivirus-business-edition", - "price": "2400.00", - "points": "240.00", - "description": "ESET Endpoint Antivirus \u2013 \u043d\u043e\u0432\u043e\u0435 \u0441\u043b\u043e\u0432\u043e \u0432 \u043f\u0440\u043e\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0437\u0430\u0449\u0438\u0442\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u0445 \u0440\u0430\u0431\u043e\u0447\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u043e\u0442 \u043b\u044e\u0431\u043e\u0433\u043e \u0438\u0437 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0434\u0443\u043a\u0442 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d \u043d\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0443 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043d\u044b\u0445 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438. \u0421\u043e\u0447\u0435\u0442\u0430\u043d\u0438\u0435 \u0437\u0430\u043f\u0430\u0442\u0435\u043d\u0442\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430 \u044d\u0432\u0440\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u0438 \u043d\u043e\u0432\u0435\u0439\u0448\u0438\u0445 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u0439 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u0442\u0438\u043f\u044b \u0443\u0433\u0440\u043e\u0437, \u0442\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0438\u0440\u0443\u044f \u043c\u043e\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u0440\u0435\u0430\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u0432\u043d\u0435\u0434\u0440\u0435\u043d\u0438\u044f \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u043e\u0433\u043e \u041f\u041e \u0432 \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u0443\u044e \u0441\u0435\u0442\u044c.", - "short_description": "", - "producer": "Eset", - "image": "products/2017/05/26/thumb_1381486034.jpg", - "discount": 0, - "stock": 10, - "category": 4, - "product_class": 1, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 87, - "fields": { - "name": "Acronis Backup 12 Workstation License", - "slug": "acronis-backup-12-workstation-license", - "price": "3190.00", - "points": "319.00", - "description": "\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438\r\n\r\n\u0411\u044b\u0441\u0442\u0440\u043e\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0438\u0441\u0442\u0435\u043c, \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u0444\u0430\u0439\u043b\u043e\u0432 \u0438 \u0434\u0430\u043d\u043d\u044b\u0445\r\n\r\n \u0411\u044b\u0441\u0442\u0440\u043e\u0435 \u0438 \u043f\u0440\u043e\u0441\u0442\u043e\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0435 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u043d\u043e\u0433\u043e \u043e\u0431\u0440\u0430\u0437\u0430 \u0434\u0438\u0441\u043a\u0430\r\n \u0423\u0434\u043e\u0431\u043d\u043e\u0435 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \"\u043d\u0430 \u0433\u043e\u043b\u043e\u0435 \u0436\u0435\u043b\u0435\u0437\u043e\" \u043d\u0430 \u0442\u043e\u043c \u0436\u0435 \u0438\u043b\u0438 \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0435\u043c\u0441\u044f \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0438, \u043b\u0438\u0431\u043e \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u0435\r\n \u0420\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0435 \u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043f\u043e\u043a \u043d\u0430 \u0434\u0438\u0441\u043a\u0435 \u0438\u043b\u0438 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043f\u0430\u043f\u043e\u043a \u043e\u0431\u0449\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f\r\n \u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u043e\u0432 \u0438 \u043f\u0430\u043f\u043e\u043a \u0438\u0437 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u043e\u0439 \u043a\u043e\u043f\u0438\u0438 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043e\u0431\u0440\u0430\u0437\u0430\r\n \u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u044b\u0445 \u043a\u043e\u043f\u0438\u0439 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043c\u0430\u0448\u0438\u043d", - "short_description": "", - "producer": "Acronis", - "image": "products/2017/05/26/thumb_1384933473.jpg", - "discount": 0, - "stock": 10, - "category": 5, - "product_class": 2, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 88, - "fields": { - "name": "Symantec System Recovery Desktop", - "slug": "symantec-system-recovery-desktop", - "price": "3830.00", - "points": "383.00", - "description": "Symantec System Recovery 2013 \u2013 \u044d\u0442\u043e \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u044b\u0445 \u043a\u043e\u043f\u0438\u0439 \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0440\u0430\u0431\u043e\u0447\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u0438 \u043d\u043e\u0443\u0442\u0431\u0443\u043a\u043e\u0432 \u0432 \u0430\u0432\u0430\u0440\u0438\u0439\u043d\u044b\u0445 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f\u0445, \u0447\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043c\u0438\u043d\u0438\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u044f \u0438 \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u0431\u044b\u0441\u0442\u0440\u043e \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u0440\u0435\u0448\u0435\u043d\u0438\u044e \u0440\u0430\u0431\u043e\u0447\u0438\u0445 \u0437\u0430\u0434\u0430\u0447.\r\n\r\n\u0422\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u044f Restore Anyware, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0435, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u043c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u044d\u0444\u0444\u0435\u043a\u0442\u0438\u0432\u043d\u043e \u0440\u0435\u0448\u0430\u0442\u044c \u0432\u043e\u043f\u0440\u043e\u0441 \u043e \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u0442\u043e\u0433\u043e, \u0447\u0435\u0433\u043e \u043d\u0443\u0436\u043d\u043e, \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f \u0438 \u0432 \u043d\u0443\u0436\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u0435. \u0414\u0430\u043d\u043d\u0430\u044f \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b\u044b, \u043f\u0430\u043f\u043a\u0438 \u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439, \u043d\u043e \u0438 \u043a\u043e\u043c\u043f\u044c\u044e\u0442\u0435\u0440 \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u043e \u0436\u0435 \u0441\u0430\u043c\u043e\u0435 \u0438\u043b\u0438 \u043d\u043e\u0432\u043e\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435.", - "short_description": "", - "producer": "Symantec", - "image": "products/2017/05/26/thumb_1401701995.png", - "discount": 0, - "stock": 5, - "category": 5, - "product_class": 2, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 89, - "fields": { - "name": "Microsoft Windows 10 Professional GetGenuine", - "slug": "microsoft-windows-10-professional-getgenuine", - "price": "12300.00", - "points": "1230.00", - "description": "Get Gunuine Windows Agreement GGWA \u2013 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0434\u043b\u044f \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043e\u043f\u0438\u0439 \u041e\u0421 Windows, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0445 \u0440\u0430\u043d\u0435\u0435 \u0431\u0435\u0437 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0438. \u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u0442 \u043e\u0442 5 \u0438 \u0431\u043e\u043b\u0435\u0435 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0439 \u0438 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u0430 \u043d\u0430 \u043a\u043e\u0440\u043f\u043e\u0440\u0430\u0442\u0438\u0432\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.", - "short_description": "", - "producer": "Microsoft", - "image": "products/2017/05/26/thumb_1438765887.jpg", - "discount": 0, - "stock": 2, - "category": 6, - "product_class": 5, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 90, - "fields": { - "name": "PERFEXPERT", - "slug": "perfexpert", - "price": "50000.00", - "points": "5000.00", - "description": "\u041a\u043e\u043c\u043f\u043b\u0435\u043a\u0441\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430, \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f, \u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u0438 \u0438\u043d\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 24\u04257. \u041e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u043c\u0438 \u0438\u043d\u0446\u0438\u0434\u0435\u043d\u0442\u0430\u043c\u0438 \u0431\u0438\u0437\u043d\u0435\u0441 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043f\u0440\u0435\u0434\u043f\u0440\u0438\u044f\u0442\u0438\u044f, \u0438 \u0442\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0430\u043c\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441 \u0441\u0438\u0441\u0442\u0435\u043c\u044b. PERFEXPERT \u043d\u043e\u0432\u044b\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441 \u0441\u0438\u0441\u0442\u0435\u043c\u044b.", - "short_description": "", - "producer": "SOFTPOINT", - "image": "products/2017/05/26/thumb_1424247372.png", - "discount": 0, - "stock": 5, - "category": 7, - "product_class": 4, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.product", - "pk": 91, - "fields": { - "name": "Office 365 Business Open", - "slug": "office-365-business-open", - "price": "5700.00", - "points": "570.00", - "description": "\u041f\u043b\u0430\u043d Office 365 \u0411\u0438\u0437\u043d\u0435\u0441 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043e\u0441\u043e\u0431\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0438 \u0446\u0435\u043d\u043e\u0432\u044b\u0435 \u043e\u0440\u0438\u0435\u043d\u0442\u0438\u0440\u044b, \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0443\u0434\u043e\u0432\u043b\u0435\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u044f \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043c\u0430\u043b\u043e\u0433\u043e \u0438 \u0441\u0440\u0435\u0434\u043d\u0435\u0433\u043e \u0431\u0438\u0437\u043d\u0435\u0441\u0430 \u2013 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0439 \u0440\u0430\u0437\u043c\u0435\u0440\u043e\u043c \u043e\u0442 1 \u0434\u043e 300 \u0441\u043e\u0442\u0440\u0443\u0434\u043d\u0438\u043a\u043e\u0432.", - "short_description": "", - "producer": "Microsoft", - "image": "products/2017/05/26/office.png", - "discount": 5, - "stock": 13, - "category": 8, - "product_class": 3, - "is_active": true, - "is_hit": false, - "is_new": false, - "created": "2017-05-26", - "updated": "2017-07-07" - } -}, -{ - "model": "products.offer", - "pk": 30, - "fields": { - "product": 85, - "name": "Kaspersky-New-One-year", - "price": "1730.00", - "points": "0.00", - "attributes": "{\"License type\": \"New\", \"License term\": \"One year\"}" - } -}, -{ - "model": "products.offer", - "pk": 31, - "fields": { - "product": 85, - "name": "Kaspersky-New-One-year", - "price": "2600.00", - "points": "0.00", - "attributes": "{\"License type\": \"New\", \"License term\": \"Two years\"}" - } -}, -{ - "model": "products.offer", - "pk": 32, - "fields": { - "product": 85, - "name": "Kaspersky-Prolongation-One-year", - "price": "1200.00", - "points": "0.00", - "attributes": "{\"License type\": \"Prolongation\", \"License term\": \"One year\"}" - } -}, -{ - "model": "products.offer", - "pk": 33, - "fields": { - "product": 85, - "name": "Kaspersky-Migration-One-year", - "price": "1900.00", - "points": "0.00", - "attributes": "{\"License type\": \"Migration\", \"License term\": \"One year\"}" - } -} -] diff --git a/products/forms.py b/products/forms.py index 63dfd50..0b82f47 100644 --- a/products/forms.py +++ b/products/forms.py @@ -1,68 +1,128 @@ -# from haystack.forms import FacetedSearchForm - - -# class FacetedProductSearchForm(FacetedSearchForm): -# def __init__(self, *args, **kwargs): -# data = dict(kwargs.get("data", [])) -# self.categories = data.get('category', []) -# self.producers = data.get('producer', []) -# super(FacetedProductSearchForm, self).__init__(*args, **kwargs) -# -# def search(self): -# sqs = super(FacetedProductSearchForm, self).search() -# if self.categories: -# query = None -# for category in self.categories: -# if query: -# query += u' OR ' -# else: -# query = u'' -# query += u'"%s"' % sqs.query.clean(category) -# sqs = sqs.narrow(u'category_exact:%s' % query) -# if self.producers: -# query = None -# for producer in self.producers: -# if query: -# query += u' OR ' -# else: -# query = u'' -# query += u'"%s"' % sqs.query.clean(producer) -# sqs = sqs.narrow(u'brand_exact:%s' % query) -# return sqs -from crispy_forms.layout import Layout, Field, Button +from ckeditor.widgets import CKEditorWidget +from crispy_forms.layout import Layout, Field, Button, Div, HTML from django import forms from crispy_forms.helper import FormHelper -from django.forms import formset_factory +from django.forms import ALL_FIELDS from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ -from .models import Product, ProductAttribute +from core.forms import QueryFormBase +from core.utils import parse_path +from products.templatetags.products_filters import apply_query_params +from .models import Product, Manufacturer -class ProductFilterForm(forms.Form): - def __init__(self, *args,**kwargs): - super().__init__(*args,**kwargs) - - -class ProductSearchForm(forms.ModelForm): +class ProductSearchForm(QueryFormBase, forms.ModelForm): field_template = 'bootstrap/forms/product_search.html' - form_action = 'products:product_list' submit_css_class = None def __init__(self, *args, **kwargs): self.submit_css_class = kwargs.pop('submit_css_class') self.helper = FormHelper() - self.helper.form_action = reverse_lazy(self.form_action) self.helper.form_method = 'get' self.helper.layout = Layout( Field('name', template=self.field_template, placeholder="Поиск программы..."), - Button(_('search'),value="search", css_class=self.submit_css_class, template=self.field_template) + Button(_('search'), value="search", css_class=self.submit_css_class, template=self.field_template) ) super().__init__(*args, **kwargs) + self.helper.form_action = reverse_lazy(**self.form_action) + apply_query_params( + self.query_params) if self.query_params else "" + class Meta: model = Product - fields = ['name'] + fields = ('name',) + + +# --------------------------- Product Filter Types -------------------------# +class ProductManufacturerFilterForm(QueryFormBase): + manufacturer = forms.ChoiceField() + + submit_css_button = None + field_template = 'bootstrap/forms/product_filter.html' + title = _('Производитель') + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'get' + self.helper.layout = Layout( + Div(HTML(self.title), css_class='category__title'), + Field('manufacturer', template=self.field_template) + ) + super().__init__(*args, **kwargs) + + self.helper.form_action = self.get_form_action_url() + + def get_initial_for_field(self, field, field_name): + if field_name == 'manufacturer': + man_qs = Manufacturer.active + category_instance = '' + if self.form_action.get('kwargs', None): + category_instance = parse_path(self.form_action.get('kwargs').get('path', '')) + + prod_qs = Product.active.filter( + parent__name=category_instance, + name__icontains=self.query_params.get('name', '') + ).only('pk') + + if prod_qs.count(): + man_qs = man_qs.filter(product__pk__in=prod_qs.all()) + + return man_qs.distinct('name').only('name', 'slug') + return super().get_initial_for_field(field, field_name) + -inline_product_filter_formset = formset_factory(ProductFilterForm,extra=ProductAttribute.objects.all(),can_order=True,can_delete=False) +class ProductSortForm(QueryFormBase): + submit_css_button = None + field_template = 'bootstrap/forms/product_sorting.html' + title = _('Сортировать по') + sort_fields = { + 'price': _('Цене'), + 'popular': _('Популярности'), + 'rate': _('Рейтингу') + } + sort = forms.ChoiceField(label=title) + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_method = 'get' + self.helper.layout = Layout( + HTML(self.title), + Field('sort', template=self.field_template) + ) + super().__init__(*args, **kwargs) + + self.helper.form_action = self.get_form_action_url() + + def get_sort_field_initial_data(self): + query_keys = self.query_params.keys() + initial_data = [] + for key in self.sort_fields: + order = "DESC" if key in query_keys else "ASC" + initial_data.append({ + 'value': "{url}?{query_params}".format(**{ + 'url': reverse_lazy(**self.form_action), + 'query_params': apply_query_params({key: order, **self.query_params}, True) + }), + 'name': self.sort_fields[key] + }) + return initial_data + + def get_initial_for_field(self, field, field_name): + if field_name == 'sort': + return self.get_sort_field_initial_data() + return super().get_initial_for_field(field, field_name) + + +# ----------------------------------------- Admin forms ------------------------------------# + + +class ProductAdminForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['description'].widget = CKEditorWidget(config_name='awesome_ckeditor') + + class Meta: + model = Product + fields = ALL_FIELDS diff --git a/products/middleware.py b/products/middleware.py new file mode 100644 index 0000000..e69de29 diff --git a/products/models.py b/products/models.py index 965fe45..7621e88 100644 --- a/products/models.py +++ b/products/models.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model from django.db import models -from django.urls import reverse_lazy +from django.db.models import Q +from django.urls import reverse_lazy, reverse from django.contrib.postgres.fields import HStoreField from django.utils.translation import ugettext_lazy as _ @@ -12,7 +13,8 @@ from mptt import ( from mptt.models import MPTTModel, TreeForeignKey from autoslug import AutoSlugField -from core.models import AbstractStatusModel, ActualOnlyManager, AbstractDateTimeModel, ActiveOnlyManager +from core.models import AbstractStatusModel, ActualOnlyManager, AbstractDateTimeModel, ActiveOnlyManager, \ + AbstractStatusMPTTModel # ---------------------------------- COMMON PRODUCT STATUS ---------------------------------------# # Create your models here. @@ -28,119 +30,136 @@ STATUS_CHOICES = ( (STATUS_DELETED, _('Удаленный')), ) -# --------------------------------- PRODUCT ATTRIBUTE STATTUS------------------------------------# -PRODUCT_ATTRIBUTE_TYPE_NONE = 0 -PRODUCT_ATTRIBUTE_TYPE_RANGE = 50 -PRODUCT_ATTRIBUTE_TYPE_SELECT = 100 - -PRODUCT_ATTRIBUTE_TYPE_CHOICES = ( - (PRODUCT_ATTRIBUTE_TYPE_NONE, _('отсуствует')), - (PRODUCT_ATTRIBUTE_TYPE_RANGE, _('диапазон')), - (PRODUCT_ATTRIBUTE_TYPE_SELECT, _('выбор')) -) -PRODUCT_ATTRIBUTE_TYPE_DEFAULT = PRODUCT_ATTRIBUTE_TYPE_NONE - - -class ProductAttributeManager(ActiveOnlyManager): - def get_range_type_attributes(self, product=None): - pass - - def get_select_type_attributes(self, product=None): - pass - - def get_all_type_attributes(self): - pass - - -class ProductAttribute(AbstractStatusModel): - name = models.CharField(max_length=64, blank=True, null=True, default=None) - slug = AutoSlugField(populate_from='name') - type = models.SmallIntegerField(_('тип'), choices=PRODUCT_ATTRIBUTE_TYPE_CHOICES, - default=PRODUCT_ATTRIBUTE_TYPE_DEFAULT) - main_attribute = models.BooleanField(default=False) - status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=STATUS_CHOICES) - - objects = ProductAttributeManager() - - def __str__(self): - return self.name - - class Meta: - ordering = ('slug',) - verbose_name = _('Аттрибут продукта') - verbose_name_plural = _('Аттрибуты продукта') +# --------------------------------- PRODUCT ATTRIBUTE STATTUS------------------------------------# +# PRODUCT_ATTRIBUTE_TYPE_NONE = 0 +# PRODUCT_ATTRIBUTE_TYPE_RANGE = 25 +# PRODUCT_ATTRIBUTE_TYPE_SELECT = 50 +# PRODUCT_ATTRIBUTE_TYPE_CHECKBOX = 75 +# PRODUCT_ATTRIBUTE_TYPE_INPUT = 100 +# +# PRODUCT_ATTRIBUTE_TYPE_CHOICES = ( +# (PRODUCT_ATTRIBUTE_TYPE_NONE, _('отсуствует')), +# (PRODUCT_ATTRIBUTE_TYPE_RANGE, _('диапазон')), +# (PRODUCT_ATTRIBUTE_TYPE_SELECT, _('выбор из списка')), +# (PRODUCT_ATTRIBUTE_TYPE_CHECKBOX, _('мн. выбор из списка')), +# (PRODUCT_ATTRIBUTE_TYPE_INPUT, _('значение'), +# ) +# ) +# PRODUCT_ATTRIBUTE_TYPE_DEFAULT = PRODUCT_ATTRIBUTE_TYPE_NONE + + +# class ProductAttributeManager(ActiveOnlyManager): +# def get_range_type_attributes(self, product=None): +# pass +# +# def get_select_type_attributes(self, product=None): +# pass +# +# def get_all_type_attributes(self): +# pass + + +# # @TODO: tranlsate into english and use traslation +# class ProductAttribute(AbstractStatusModel): +# name = models.CharField(_('название'), max_length=64) +# slug = AutoSlugField(populate_from='name', verbose_name=_('код'), help_text="поисковый код аттрибута,по котором возможен поиск") +# type = models.SmallIntegerField(_('тип'), choices=PRODUCT_ATTRIBUTE_TYPE_CHOICES, +# default=PRODUCT_ATTRIBUTE_TYPE_DEFAULT) +# status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=STATUS_CHOICES) +# main_attribute = models.BooleanField(_('основной атрибут'), default=False, help_text=_('будет отображен рядом с товаром при отображении в списке')) +# +# objects = ProductAttributeManager() +# +# def __str__(self): +# return self.name +# +# class Meta: +# ordering = ('slug',) +# verbose_name = _('Аттрибут продукта') +# verbose_name_plural = _('Аттрибуты продукта') + + +# @TODO: tranlsate into english and use traslation class Manufacturer(AbstractStatusModel): - name = models.CharField(max_length=64, blank=True, null=True, default=None) - slug = AutoSlugField(populate_from='name') - image = models.ImageField(upload_to='producers', blank=True, null=True, verbose_name=("Изображение")) + name = models.CharField(_('название'), max_length=64, blank=True, null=True, default=None) + slug = AutoSlugField(verbose_name=_('код'), unique=True, populate_from='name') + image = models.ImageField(_('изображение'), upload_to='producers', blank=True, null=True) status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=STATUS_CHOICES) def __str__(self): return self.name - def get_absolute_url(self): - return reverse_lazy('products:manufacturer', kwargs={'producer_slug': self.slug, 'path': ''}) - - # @TODO: tranlsate into english and use traslation class Meta: verbose_name = _('Производитель') verbose_name_plural = _('Производители') -class ProductCategoryManager(mptt_managers.TreeManager, ActualOnlyManager): - def get_categories(self, parent=None): - return self.get_queryset().filter(parent=parent).all() - - -class ProductActiveCategoryManager(mptt_models.TreeManager, ActiveOnlyManager): - def get_categories(self, parent=None): - return self.get_queryset().filter(parent=parent).all() - +# @TODO: tranlsate into english and use traslation +class ProductCategory(AbstractStatusMPTTModel): + def get_file_path(self, filename): + return "products/{category}/{filename}".format(**{ + 'category': self.slug, + 'filename': filename + }) -class ProductCategory(MPTTModel, AbstractStatusModel): name = models.CharField(_('название'), db_index=True, unique=True, max_length=64, blank=True, null=True, default=None) - slug = AutoSlugField(_('slug'), populate_from='name') + slug = AutoSlugField(_('код'), populate_from='name') parent = TreeForeignKey('self', verbose_name=_('родительская категория'), on_delete=models.CASCADE, null=True, blank=True, related_name='children') - image = models.ImageField(_("иконка"), upload_to='categories', blank=True) + image = models.FileField(_("иконка"), upload_to=get_file_path, blank=True) status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=STATUS_CHOICES) - objects = ProductCategoryManager() - active = ProductActiveCategoryManager() - def __str__(self): return self.name + @property + def viewname(self): + return 'products:product_list' + + @property + def viewname_kwargs(self): + return {'path': self.get_path()} + + class MPTTMeta: + order_insertion_by = ('name',) + # @TODO: tranlsate into english and use traslation class Meta: + unique_together = ('slug', 'parent') + ordering = ('tree_id', 'level') verbose_name = _("Категория") verbose_name_plural = _("Категории") - ordering = ('tree_id', 'level') - - class MPTTMeta: - order_insertion_by = ['name'] - -register(ProductCategory, order_insertion_py=['name']) - -class Product(AbstractStatusModel): +# @TODO: translate into english and use translation +class Product(AbstractStatusMPTTModel): name = models.CharField(_('имя'), max_length=64, db_index=True) slug = AutoSlugField(_('slug'), populate_from='name', db_index=True) + description = models.TextField(_('описание'), blank=True, null=True, default=None) - manufacturer = models.ForeignKey(Manufacturer, on_delete=models.SET_NULL, blank=True, null=True) - category = models.ForeignKey(ProductCategory, on_delete=models.SET_NULL, blank=True, null=True) - attributes = models.ManyToManyField(ProductAttribute, blank=True) + manufacturer = models.ForeignKey(Manufacturer, verbose_name=_('производитель'), on_delete=models.SET_NULL, + blank=True, null=True) + parent = TreeForeignKey(ProductCategory, verbose_name=_('категория'), on_delete=models.SET_NULL, blank=True, + null=True, help_text="Категория") + platform = models.CharField(_('Платформа'), max_length=255, null=True, blank=True) status = models.SmallIntegerField(_('статус'), default=STATUS_DEFAULT, choices=STATUS_CHOICES) def __str__(self): return self.name - def get_absolute_url(self, request): - return request.build_absolute_uri(reverse_lazy('products:item', args=[self.slug])) + @property + def viewname(self): + return 'products:product_details' + + @property + def viewname_kwargs(self): + return {'path': self.parent.get_path() + self.get_path()} + + class MPTTMeta: + order_insertion_by = ('name',) class Meta: indexes = [ @@ -150,72 +169,75 @@ class Product(AbstractStatusModel): verbose_name_plural = _('Продукты') - - # def save(self, *args, **kwargs): - # if self.category: - # super(Product, self).save(*args, **kwargs) - # - # for cp in ProductClass.objects.filter(category=self.product_class): - # pp = ProductProperty.objects.filter(category_property=cp, - # products=self) - # if not pp: - # pp = ProductProperty(category_property=cp, products=self, value="--") - # pp.save() - - +# @TODO: tranlsate into english and use traslation class ProductRate(AbstractDateTimeModel): user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) - rate = models.IntegerField(_('оценка'), default=0) + rate = models.PositiveSmallIntegerField(_('оценка'), default=0) class Meta: verbose_name = _('Рейтинг продукта') verbose_name_plural = _('Рейтинг продукта') -class ProductDiscount(AbstractStatusModel): - product = models.ForeignKey(Product, on_delete=models.CASCADE) - percentage = models.DecimalField(_('процент'), max_digits=3, decimal_places=2) - status = models.SmallIntegerField(_('статус'), choices=STATUS_CHOICES, default=STATUS_DEFAULT) +# +# # @TODO: translate into english and use traslation +# class ProductAttributeValue(AbstractDateTimeModel): +# attribute = models.ForeignKey(ProductAttribute, on_delete=models.CASCADE, related_name='value') +# value = HStoreField(_('значение'),default="") +# +# def __str__(self): +# return self.value.serialize +# +# class Meta: +# unique_together = ('attribute', 'value') +# verbose_name = _('Значение аттрибута') +# verbose_name_plural = _('Значение аттрибутов') - class Meta: - verbose_name = _('Дисконт') - verbose_name_plural = _('Дисконты') +# ----------------- PRODUCT IMAGE STATUS LIST ------------------ -class ProductAttributeValue(AbstractStatusModel): - attribute = models.ForeignKey(ProductAttribute, on_delete=models.CASCADE, related_name='value') - value = HStoreField(_('значение'), default={}) +class ProductImageManager(models.Manager): + def get_default_image(self): + return self.get_queryset().filter(product=self.instance, is_default=True).first() - def __str__(self): - return self.value.serialize + def get_all_images(self): + return self.get_queryset().filter(product=self.instance).order_by('-is_default').all() - class Meta: - unique_together = ('attribute', 'value') - verbose_name = _('Значение аттрибута') - verbose_name_plural = _('Значение аттрибутов') + def get_all_images_except_default(self): + return self.get_queryset().filter(product=self.instance, is_default=False).all() -# ----------------- PRODUCT IMAGE STATUS LIST ------------------ - -class ProductImage(AbstractStatusModel): +class ProductImage(AbstractDateTimeModel): def get_file_path(self, filename): - return "products/attachments/{product}/{filename}".format(**{ - 'product': self.product.id, + return "products/{product}/{filename}".format(**{ + 'product': self.product.slug, 'filename': filename }) product = models.ForeignKey(Product, on_delete=models.CASCADE) - status = models.SmallIntegerField(_('Статус'), choices=STATUS_CHOICES, default=STATUS_DEFAULT) - filename = models.CharField(_('Имя файла'), max_length=255) - image = models.FileField(_('Изображение'), upload_to=get_file_path, max_length=500) - is_default = models.BooleanField(_('По умолчанию'), default=False) + filename = models.CharField(_('имя файла'), max_length=255) + image = models.FileField(_('изображение'), upload_to=get_file_path, max_length=500) + is_default = models.BooleanField(_('по умолчанию'), default=False) + + objects = ProductImageManager() @classmethod def create(cls, request, file): product_image = cls(request=request, file=file, filename=file.name) return product_image + def __str__(self): + return self.filename + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + if not self.product.productimage_set.count(): + self.is_default = True + elif self.is_default: + self.product.productimage_set.update(is_default=False) + self.filename = self.image.name + return super().save(force_insert, force_update, using, update_fields) + class Meta: verbose_name = _('Изображение продукта') - verbose_name_plural = _('Изображения продуктов') + verbose_name_plural = _('Изображения продукта') diff --git a/products/templatetags/__init__.py b/products/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/templatetags/product_tags.py b/products/templatetags/product_tags.py new file mode 100644 index 0000000..5d16ee8 --- /dev/null +++ b/products/templatetags/product_tags.py @@ -0,0 +1,7 @@ +from django.template import Library +from django.utils.translation import ugettext_lazy as _ + + +register = Library() + + diff --git a/products/templatetags/products_filters.py b/products/templatetags/products_filters.py new file mode 100644 index 0000000..8903a0c --- /dev/null +++ b/products/templatetags/products_filters.py @@ -0,0 +1,61 @@ +from functools import reduce +from django.utils.translation import ugettext_lazy as _ + +from django.template import Library + +from cart.forms import CartAddInlineForm + +register = Library() + + +@register.filter +def apply_nds_status(doesNdsInclude): + return _('Включено') if doesNdsInclude else _('Не включено') + + +@register.filter +def apply_product_offer_form(product): + initial = { + 'offer': product.id, + 'amount': 1 + } + return CartAddInlineForm(initial=initial) + + +@register.filter +def apply_query_params(params, doAppend=False): + """ + + :param params dict: + :param arg return as additional query params or initial: + :return string: + """ + formated_params = "" + if not params: + return formated_params + if doAppend: + formated_params = "&" + formated_params + formated_params = reduce( + lambda q_str, q_key: q_str + "{key}={val}&".format(**{'key': q_key, 'val': params[q_key]}), + params, + formated_params + ) + return formated_params[:-1] + + +@register.filter +def filter_query_params(params, query_key): + new_params = {**params} if isinstance(params, dict) else {} + if query_key in new_params: + new_params.pop(query_key) + return new_params + + +@register.filter +def get_item(dictionary, key): + return dictionary.get(key) + + +@register.filter +def apply_desc_preview(description): + return description diff --git a/products/urls.py b/products/urls.py index 1d58244..35f0ff2 100644 --- a/products/urls.py +++ b/products/urls.py @@ -16,15 +16,22 @@ Including another URLconf from django.urls import re_path import mptt_urls from . import views -from .models import ProductCategory - +from .models import ProductCategory, Product urlpatterns = [ re_path(r'^list/$', views.ProductListView.as_view(), name='product_list'), - re_path(r'^list/(?P\w+)/$', views.ProductListView.as_view(),name='product_list'), - - re_path(r'^(?P\w+)/$', views.ProductDetailsView.as_view(), name='product_details'), + re_path( + r'^list/(?P.*)', + mptt_urls.view(model=ProductCategory, view=views.ProductListView.as_view(),slug_field='slug'), + name='product_list' + ), + + re_path( + r'^item/(?P.*)', + mptt_urls.view(model=Product, view=views.ProductDetailView.as_view(),slug_field='slug'), + name='product_details' + ), # Uncomment for elasticsearch @@ -36,6 +43,4 @@ urlpatterns = [ # url(r'^(?P[-\w]+)/(?P.*)', # mptt_urls.view(model=ProductCategory, view=categorieslist, slug_field='slug'), # name='CategoriesListByProducer'), - # url(r'^(?P[-\w]+)/$', categorieslist, name='CategoriesListByProducer'), - # url(r'^(?P[-\w]+)/(?P[-\w]+)/$', productslist, name='ProductListByCategory') ] diff --git a/products/views.py b/products/views.py index f1688dc..805b8b7 100644 --- a/products/views.py +++ b/products/views.py @@ -1,151 +1,95 @@ -import json -import decimal +from functools import reduce from django.conf import settings -from django.shortcuts import render -from django.contrib import auth +from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _ from django.views.generic import ListView, DetailView -from core.views import ProtectedListView -from .models import ( - Manufacturer, Product, ProductCategory -) +from cart.forms import CartAddInlineForm +from core.utils import parse_path +from .forms import (ProductSortForm) +from .models import (Product,ProductCategory) -from .forms import * +class ProductDetailView(DetailView): + http_method_names = ('get',) + model = Product + pk_url_kwarg = None + slug_url_kwarg = 'path' + context_object_name = 'product' + template_name = 'products/product_detail.html' -class ManufactureListView(ListView): - model = Manufacturer - template_name = 'products/manufacture_list.html' + def dispatch(self, request, *args, **kwargs): + if self.kwargs.get(self.slug_url_kwarg): + slug_url_kwarg_val = self.kwargs.pop(self.slug_url_kwarg) + slug_url_kwarg_val = parse_path(slug_url_kwarg_val) + self.kwargs[self.slug_url_kwarg] = slug_url_kwarg_val + return super().dispatch(request, *args, **kwargs) + def get_slug_field(self): + return 'slug' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = self.object.name + return context -class ProductDetailsView(DetailView): - model = Product - template_name = 'products/product_detail.html' - title = _('Продукт') class ProductListView(ListView): + http_method_names = ('get',) model = Product - form = ProductSearchForm template_name = 'products/product_list.html' context_object_name = 'products' paginate_by = settings.DEFAULT_PAGE_AMOUNT - title = _('Список товаров') + title = _('Каталог') - def get_title(self): + def filter_products(self,qs): + if self.kwargs.get('path'): + category_intance = parse_path(self.kwargs.get('path')) + cat_qs = ProductCategory.objects.filter(slug__exact=category_intance).first() + qs = qs.filter(parent__in=cat_qs.get_descendants(include_self=True)) if self.request.GET.get('name'): - return _('Поиск товара') + ":" + self.request.GET.get('name') - title = _('Список товаров') - if self.request.resolver_match.kwargs.get('category_slug'): - return title + ":" + self.request.resolver_match.kwargs.get('category_slug') - return title + qs = qs.filter(name__icontains=self.request.GET.get('name')) + if self.request.GET.get('manufacturer'): + qs = qs.filter(manufacturer__slug=self.request.GET.get('manufacturer')) + if self.request.GET.get('supply_type'): + qs = qs.filter(offer__supply_type__slug=self.request.GET.get('supply_type')) + if self.request.GET.get('supply_target'): + qs = qs.filter(offer__supply_target__slug=self.request.GET.get('supply_target')) + qs = qs.filter(offer__amount__gte=0) + return qs + + def sort_products(self,qs): + if not self.request.GET: + return qs + if self.request.GET.get('sort'): + qs = qs.order_by() + return qs def get_queryset(self): qs = super().get_queryset() - if self.kwargs.get('category_slug'): - qs = qs.filter(category__slug=self.kwargs.get('category_slug')) - if self.request.GET.get('name'): - qs = qs.filter(name__icontains=self.request.GET.get('name')) + qs = self.filter_products(qs) + qs = self.sort_products(qs) return qs + def get_cart_add_formset(self, products): + return { product.id: CartAddInlineForm(initial={'offer': product.id,'amount': 1}) for product in products} + + + def get_product_sorting_form(self): + if not "products" in self.request.resolver_match.view_name: + product_form_action = {'viewname': 'products:product_list', 'kwarg': {}} + else: + product_form_action = {'viewname': self.request.resolver_match.view_name, + 'kwargs': self.request.resolver_match.kwargs} + return ProductSortForm( + product_form_action=product_form_action, + query_params=self.request.GET + ) + def get_context_data(self, *, object_list=None, **kwargs): context = super().get_context_data(object_list=object_list, **kwargs) - context['title'] = self.get_title() + context['title'] = self.title + context['product_sort_form'] = self.get_product_sorting_form() + context['product_cart_add_formset'] = self.get_cart_add_formset(self.object_list) return context - -# Uncomment for elasticsearch - -# from .layout import FacetedProductSearchForm -# from haystack.generic_views import FacetedSearchView as BaseFacetedSearchView -# from haystack.query import SearchQuerySet - -# def serialize_decimal(obj): -# if isinstance(obj, decimal.Decimal): -# return str(obj) -# return json.JSONEncoder.default(obj) -# -# -# def producerslist(request): -# username = auth.get_user(request).username -# # category = None -# # categories = ProductCategory.objects.filter(level__lte=0) -# # products = Product.objects.filter(is_active=True) -# producers = Manufacturer.objects.filter(is_active=True) -# # if category_slug: -# # category = get_object_or_404(ProductCategory, slug=category_slug) -# # products = products.filter(category__in=category.get_descendants(include_self=True)) -# return render(request, 'products/list.html', locals()) - -# def categorieslist(request, producer_slug, category_slug=None): -# username = accounts_ext.get_user(request).username -# producer = Producer.objects.get(slug=producer_slug) -# if category_slug: -# _categories = ProductCategory.objects.filter(is_active=True, parent=category_slug) -# else: -# _categories = ProductCategory.objects.filter(is_active=True, producer=producer, level__lte=0) -# categories, products = expand_categories(_categories) -# return render(request, 'products/categorieslist.html', {'username': username, 'categories':categories, -# 'products': products}) -# -# def categorieslist(request, path, instance, producer_slug): -# username = auth.get_user(request).username -# if instance: -# _categories = instance.get_children() -# else: -# _categories = get_list_or_404(ProductCategory, producer__slug=producer_slug, level__lte=0) -# if _categories: -# categories, products = expand_categories(_categories) -# else: -# return productslist(request, producer_slug, instance.slug) -# return render( -# request, -# 'products/categorieslist.html', -# { -# 'username': username, -# 'instance': instance, -# 'categories': categories, -# 'producer_slug': producer_slug, -# 'products': products -# } -# ) -# -# def productslist(request, producer_slug, category_slug): -# username = auth.get_user(request).username -# category = ProductCategory.objects.get(slug=category_slug) -# products = Product.objects.filter(is_active=True, category=category) -# return render(request, 'products/productslist.html', locals()) -# -# def product(request, product_slug): -# username = auth.get_user(request).username -# product = get_object_or_404(Product, slug=product_slug, is_active=True) -# cart_product_form = CartAddProductForm() -# variant_picker_data = get_variant_picker_data(product) -# show_variant_picker = all([v.attributes for v in product.variants.all()]) -# # session_key = request.session.session_key -# # if not session_key: -# # request.session.cycle_key() -# -# return render(request, 'products/product.html', {'username': username, 'products': product, 'form': cart_product_form, -# 'show_variant_picker': show_variant_picker, -# 'variant_picker_data': variant_picker_data, -# }) - -# Uncomment for elasticsearch - -# def autocomplete(request): -# sqs = SearchQuerySet().autocomplete(content_auto=request.GET.get('query', ''))[:5] -# s = [] -# for result in sqs: -# print(result) -# d = {"value": result.name, "data": result.object.slug} -# s.append(d) -# output = {'suggestions': s} -# return JsonResponse(output) -# -# class FacetedSearchView(BaseFacetedSearchView): -# form_class = FacetedProductSearchForm -# facet_fields = ['category', 'producer'] -# template_name = 'search/search.html' -# paginate_by = 3 -# context_object_name = 'object_list'