From 8a940985469ab5a93e3498ca778963799d0ef6d5 Mon Sep 17 00:00:00 2001 From: Max Yakovenko Date: Thu, 12 Jul 2018 23:58:31 +0300 Subject: [PATCH] add products app --- products/__init__.py | 0 products/admin.py | 195 ++++++++++++ products/apps.py | 5 + products/context_processors.py | 10 + products/fixtures/products.json | 533 ++++++++++++++++++++++++++++++++ products/forms.py | 56 ++++ products/models.py | 137 ++++++++ products/search_indexes.py | 20 ++ products/tests.py | 3 + products/urls.py | 39 +++ products/utils.py | 39 +++ products/views.py | 116 +++++++ 12 files changed, 1153 insertions(+) create mode 100644 products/__init__.py create mode 100644 products/admin.py create mode 100644 products/apps.py create mode 100644 products/context_processors.py create mode 100644 products/fixtures/products.json create mode 100644 products/forms.py create mode 100644 products/models.py create mode 100644 products/search_indexes.py create mode 100644 products/tests.py create mode 100644 products/urls.py create mode 100644 products/utils.py create mode 100644 products/views.py diff --git a/products/__init__.py b/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/admin.py b/products/admin.py new file mode 100644 index 0000000..ee6311e --- /dev/null +++ b/products/admin.py @@ -0,0 +1,195 @@ +# from django.contrib import admin +# from import_export import resources, fields, widgets +# from import_export.admin import ImportExportModelAdmin +# from .models import * +# +# class CustomModelResource(resources.ModelResource): +# def before_import_row(self, row, **kwargs): +# """ +# Override to add additional logic. Does nothing by default. +# """ +# try: +# row['attributes'] = eval(row['attributes']) +# except: +# try: +# row['discount_policy'] = eval(row['discount_policy']) +# except: +# pass +# +# class CustomManyToManyWidget(widgets.ManyToManyWidget): +# def clean(self, value, row=None, *args, **kwargs): +# t1 = super(CustomManyToManyWidget, self).clean(value) +# 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 = ProductAttributeChoiceValue +# # prepopulated_fields = {'slug': ('name',)} +# extra = 1 +# verbose_name_plural = 'AttributeChoiceValue' +# suit_classes = 'suit-tab suit-tab-ACV' +# # +# # class OfferInline(admin.TabularInline): +# # model = Offer +# # extra = 1 +# # verbose_name_plural = 'Offers' +# # suit_classes = 'suit-tab suit-tab-offers' +# +# class ProductCategoryAdmin(admin.ModelAdmin): +# list_display = [field.name for field in ProductCategory._meta.fields] +# +# class Meta: +# model = ProductCategory +# +# # class AttributeChoiceValueAdmin(admin.ModelAdmin): +# # list_display = [field.name for field in ProductCategory._meta.fields] +# # +# # class Meta: +# # model = AttributeChoiceValue +# # +# # admin.site.register(AttributeChoiceValue, AttributeChoiceValueAdmin) +# +# class ProductAttributeAdmin(admin.ModelAdmin): +# list_display = [field.name for field in ProductAttribute._meta.fields] +# inlines = [AttributeChoiceValueInline] +# # prepopulated_fields = {'slug': ('name',)} +# +# suit_form_tabs = (('general', 'General'), +# ('ACV', 'AttributeValues'),) +# +# class Meta: +# model = ProductAttribute +# +# admin.site.register(ProductAttribute, ProductAttributeAdmin) +# +# @admin.register(Manufacturer) +# class ProducerAdmin(admin.ModelAdmin): +# list_display = [field.name for field in Manufacturer._meta.fields] +# +# +# class ProductResource(CustomModelResource): +# # id = fields.Field(default=generate_Jid(prefix='J'), +# # readonly=True, +# # widget=widgets.CharWidget(), +# # ) +# +# 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(), +# # ) +# 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', +# default=None, +# 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', +# 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') +# export_order = ('id', 'name', 'producer', 'is_active', 'category', 'attributes', 'description', 'discount_policy') +# # import_id_fields = ('name',) +# +# def dehydrate_str_choices(self, obj): +# if obj.id: +# return obj.str_choices() +# +# @admin.register(Product) +# class ProductAdmin(ImportExportModelAdmin): +# list_display = ['id', 'name', 'category', 'manufacturer','status'] +# # inlines = [OfferInline] +# 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 +# # class ProductImageAdmin(admin.ModelAdmin): +# # list_display = [field.name for field in ProductImage._meta.fields] +# # +# # class Meta: +# # model = ProductImage +# +# # admin.site.register(ProductImage, ProductImageAdmin) +# # admin.site.register(ProductCategory, ProductCategoryAdmin) +# # admin.site.register(Product, ProductAdmin) +# # admin.site.register(Offer, OfferAdmin) diff --git a/products/apps.py b/products/apps.py new file mode 100644 index 0000000..864c43e --- /dev/null +++ b/products/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + name = 'products' diff --git a/products/context_processors.py b/products/context_processors.py new file mode 100644 index 0000000..dd2871f --- /dev/null +++ b/products/context_processors.py @@ -0,0 +1,10 @@ + +from products.forms import ProductSearchForm + + +def product_search_form(request): + return {'product_search_form': ProductSearchForm()} + +def product_root_categories(request): + categories = {'product_root_categories': []} + return categories diff --git a/products/fixtures/products.json b/products/fixtures/products.json new file mode 100644 index 0000000..2e21761 --- /dev/null +++ b/products/fixtures/products.json @@ -0,0 +1,533 @@ +[ + { + "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 new file mode 100644 index 0000000..4ba581c --- /dev/null +++ b/products/forms.py @@ -0,0 +1,56 @@ +# 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, ButtonHolder, Submit, HTML, Field, Button +from django import forms +from crispy_forms.helper import FormHelper +from django.urls import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from .models import Product + + +class ProductSearchForm(forms.ModelForm): + field_template = 'bootstrap/forms/product_search.html' + + def __init__(self, *args, **kwargs): + self.helper = FormHelper() + self.helper.form_action = reverse_lazy('products:search') + self.helper.form_method = 'get' + self.helper.layout = Layout( + Field('name', template=self.field_template, placeholder="Поиск программы..."), + Button(_('search'), 'search', template=self.field_template) + ) + super().__init__(*args, **kwargs) + + class Meta: + model = Product + fields = ['name'] diff --git a/products/models.py b/products/models.py new file mode 100644 index 0000000..d0e5ea4 --- /dev/null +++ b/products/models.py @@ -0,0 +1,137 @@ +from django.db import models +from django.urls import reverse_lazy +from django.contrib.postgres.fields import HStoreField +from django.utils.translation import ugettext_lazy as _ + +import mptt +from mptt.models import MPTTModel, TreeForeignKey +from autoslug import AutoSlugField + +from core.models import AbstractStatusModel, AbstractDateTimeModel + + +class ProductAttribute(AbstractStatusModel): + name = models.CharField(max_length=64, blank=True, null=True, default=None) + slug = AutoSlugField(populate_from='name') + main_attribute = models.BooleanField(default=False) + + def __str__(self): + return self.name + + class Meta: + ordering = ('slug',) + verbose_name = _('Product attribute') + verbose_name_plural = _('Product attributes') + + +class ProductAttributeChoiceValue(AbstractDateTimeModel): + name = models.CharField(max_length=64, blank=True, null=True, default=None) + slug = AutoSlugField(populate_from='name') + attribute = models.ForeignKey(ProductAttribute, on_delete=models.CASCADE, related_name='values') + + def __str__(self): + return self.name + + class Meta: + unique_together = ('name', 'attribute') + verbose_name = 'attribute choices value' + verbose_name_plural = 'attribute choices values' + + +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, verbose_name="image of producer") + + def __str__(self): + return self.name + + def get_absolute_url(self): + # return reverse('products:CategoriesListByProducer', args=[self.slug]) + return reverse_lazy('products:manufacturer', kwargs={'producer_slug': self.slug, 'path': ''}) + + class Meta: + verbose_name = 'Producer' + verbose_name_plural = 'Producers' + + +class ProductCategory(MPTTModel, AbstractDateTimeModel): + name = models.CharField(db_index=True, unique=True, max_length=64, blank=True, null=True, default=None) + slug = AutoSlugField(populate_from='name') + parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + image = models.ImageField(upload_to='categories', blank=True, verbose_name="image of category") + + def __str__(self): + return self.name + + class Meta: + verbose_name = 'Product''s category' + verbose_name_plural = 'Category of products' + ordering = ('tree_id', 'level') + + class MPTTMeta: + order_insertion_by = ['name'] + + +mptt.register(ProductCategory, order_insertion_py=['name']) + + +class Product(AbstractStatusModel): + name = models.CharField(max_length=64, db_index=True, blank=True, null=True, default=None) + slug = AutoSlugField(populate_from='name') + price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + description = models.TextField(db_index=True, blank=True, null=True, default=None) + manufacturer = models.ForeignKey(Manufacturer, on_delete=models.PROTECT, related_name='products') + image = models.ImageField(upload_to='products', blank=True, verbose_name="image of products") + category = models.ForeignKey(ProductCategory, on_delete=models.SET_NULL, related_name='products', blank=True, + null=True, default=None) + attributes = models.ManyToManyField(ProductAttribute, related_name='categories', blank=True) + discount_policy = HStoreField(blank=True, null=True, default={}) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse_lazy('products:item', args=[self.slug]) + + class Meta: + indexes = [ + models.Index(fields=['id', 'slug']) + ] + verbose_name = _('product') + verbose_name_plural = _('products') + + + + # 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() + +# class Offer(models.Model): +# name = models.CharField(max_length=64, blank=True, null=True, default=None) +# slug = AutoSlugField(populate_from='name') +# price = models.DecimalField(max_digits=8, decimal_places=2, null=True, default=0.00) +# # points = models.DecimalField(max_digits=8, decimal_places=2, null=True, default=0.00) +# products = models.ForeignKey(Product, on_delete=models.CASCADE, blank=True, null=True, default=None, +# related_name='variants') +# is_active = models.BooleanField(default=True) +# attributes = HStoreField(blank=True, null=True, default={}) +# +# def __str__(self): +# return self.name +# +# class Meta: +# verbose_name = 'Offer' +# verbose_name_plural = 'Offers' +# +# def save(self, *args, **kwargs): +# self.points = self.price * decimal.Decimal('0.1') +# super(Offer, self).save(*args, **kwargs) diff --git a/products/search_indexes.py b/products/search_indexes.py new file mode 100644 index 0000000..78bc4eb --- /dev/null +++ b/products/search_indexes.py @@ -0,0 +1,20 @@ +import datetime +from haystack import indexes +from .models import * + +class ProductIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.EdgeNgramField(document=True, use_template=True, template_name="search/product_text.txt") + name = indexes.EdgeNgramField(model_attr='name') + description = indexes.EdgeNgramField(model_attr='description') + category = indexes.CharField(model_attr='category', faceted=True) + producer = indexes.CharField(model_attr='producer', faceted=True) + + content_auto = indexes.EdgeNgramField(model_attr='name') + + suggestions = indexes.FacetCharField() + + def get_model(self): + return Product + + def index_queryset(self, using=None): + return self.get_model().objects.all() \ No newline at end of file diff --git a/products/tests.py b/products/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/products/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/products/urls.py b/products/urls.py new file mode 100644 index 0000000..d0c4c4a --- /dev/null +++ b/products/urls.py @@ -0,0 +1,39 @@ +"""Eshop URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.urls import re_path +import mptt_urls +from . import views +from .models import ProductCategory + + +urlpatterns = [ + re_path(r'^search/$', views.ProductSearchView.as_view(), name='search') + #url(r'^products/(?P\w+)/$', views.products, name='products'), + # url(r'^$', producerslist, name='ProductList'), + + # Uncomment for elasticsearch + + # url(r'^autocomplete/$', autocomplete), + # url(r'^find/$', FacetedSearchView.as_view(), name='haystack_search'), + + + # url(r'^products/(?P[-\w]+)/$', products, name='Product'), + # 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/utils.py b/products/utils.py new file mode 100644 index 0000000..cd665be --- /dev/null +++ b/products/utils.py @@ -0,0 +1,39 @@ +from .models import Product + +def get_variant_picker_data(product): + variants = product.variants.all() + variant_attributes = product.attributes.all() + data = {'variants': [], 'variantAttributes': [], 'discount_policy': product.discount_policy} + + for attribute in sorted(variant_attributes, key=lambda x: x.main_attribute, reverse=True): + data['variantAttributes'].append({ + 'name': attribute.name, + 'public_name': attribute.name.split('_')[1], + 'slug': attribute.slug, + 'values': [{'name': value.name, 'slug': value.slug} for value in attribute.values.all()] + }) + + for variant in variants: + price = variant.price + + variant_data = { + 'id': variant.id, + 'slug': variant.slug, + 'name': variant.name, + 'price': int(price), + 'attributes': variant.attributes, + + } + + data['variants'].append(variant_data) + + return data + +def expand_categories(categories): + products = None + for e in categories: + if e.name.startswith('None'): + products = Product.objects.filter(category=e) + return [x for x in categories if not x.name.startswith('None')], products + + diff --git a/products/views.py b/products/views.py new file mode 100644 index 0000000..be31809 --- /dev/null +++ b/products/views.py @@ -0,0 +1,116 @@ +from django.shortcuts import render, get_list_or_404, get_object_or_404 +from django.contrib import auth +from django.http import Http404 +import json +import decimal + +from django.views.generic import ListView + +from cart.forms import CartAddProductForm +from .utils import * +from cart.cart import Cart +from .models import * + + +class ProductSearchView(ListView): + model = Product + template_name = 'products/search.html' + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(name__icontains=self.request) + +# 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 = Producer.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'