diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 50c21b90..66f0aee1 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from comment.models import Comment from establishment import models from main.models import Award +from product.models import Product from review import models as review_models @@ -46,13 +47,18 @@ class CommentInline(GenericTabularInline): extra = 0 +class ProductInline(admin.TabularInline): + model = Product + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] inlines = [ AwardInline, ContactPhoneInline, ContactEmailInline, - ReviewInline, CommentInline] + ReviewInline, CommentInline, ProductInline] @admin.register(models.Position) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 9648c6b0..0ca2463f 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -486,6 +486,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): """ return self.id + @property + def wines(self): + """Return list products with type wine""" + return self.products.wines() + class Position(BaseAttributes, TranslatedFieldsMixin): """Position model.""" diff --git a/apps/product/admin.py b/apps/product/admin.py new file mode 100644 index 00000000..b3dbc0cd --- /dev/null +++ b/apps/product/admin.py @@ -0,0 +1,18 @@ +"""Product admin conf.""" +from django.contrib import admin +from .models import Product, ProductType, ProductSubType + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + """Admin page for model Product.""" + + +@admin.register(ProductType) +class ProductTypeAdmin(admin.ModelAdmin): + """Admin page for model ProductType.""" + + +@admin.register(ProductSubType) +class ProductSubTypeAdmin(admin.ModelAdmin): + """Admin page for model ProductSubType.""" diff --git a/apps/product/filters.py b/apps/product/filters.py new file mode 100644 index 00000000..9318b943 --- /dev/null +++ b/apps/product/filters.py @@ -0,0 +1,34 @@ +"""Filters for app Product.""" +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from product import models + + +class ProductListFilterSet(filters.FilterSet): + """Product filter set.""" + + establishment_id = filters.NumberFilter() + type = filters.ChoiceFilter(method='by_type', + choices=models.ProductType.INDEX_NAME_TYPES) + subtype = filters.ChoiceFilter(method='by_subtype', + choices=models.ProductSubType.INDEX_NAME_TYPES) + + class Meta: + """Meta class.""" + model = models.Product + fields = [ + 'establishment_id', + 'type', + 'subtype', + ] + + def by_type(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_type(value) + return queryset + + def by_subtype(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_subtype(value) + return queryset diff --git a/apps/product/models.py b/apps/product/models.py index 41f0c7c6..d629d663 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -1,5 +1,6 @@ """Product app models.""" from django.db import models +from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField from django.utils.translation import gettext_lazy as _ from utils.models import (BaseAttributes, ProjectBaseMixin, @@ -9,9 +10,23 @@ from utils.models import (BaseAttributes, ProjectBaseMixin, class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): """ProductType model.""" + STR_FIELD_NAME = 'name' + + # INDEX NAME CHOICES + FOOD = 'food' + WINE = 'wine' + LIQUOR = 'liquor' + + INDEX_NAME_TYPES = ( + (FOOD, _('Food')), + (WINE, _('Wine')), + (LIQUOR, _('Liquor')), + ) + name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, unique=True, db_index=True, + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, verbose_name=_('Index name')) use_subtypes = models.BooleanField(_('Use subtypes'), default=True) @@ -25,19 +40,35 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): """ProductSubtype model.""" + STR_FIELD_NAME = 'name' + + # INDEX NAME CHOICES + RUM = 'rum' + OTHER = 'other' + + INDEX_NAME_TYPES = ( + (RUM, _('Rum')), + (OTHER, _('Other')), + ) + product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, related_name='subtypes', verbose_name=_('Product type')) name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, unique=True, db_index=True, + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, verbose_name=_('Index name')) class Meta: """Meta class.""" - verbose_name = _('Product type') - verbose_name_plural = _('Product types') + verbose_name = _('Product subtype') + verbose_name_plural = _('Product subtypes') + + def clean_fields(self, exclude=None): + if not self.product_type.use_subtypes: + raise ValidationError(_('Product type is not use subtypes.')) class ProductManager(models.Manager): @@ -47,16 +78,33 @@ class ProductManager(models.Manager): class ProductQuerySet(models.QuerySet): """Product queryset.""" + def with_base_related(self): + return self.select_related('country', 'product_type', 'establishment') \ + .prefetch_related('product_type__subtypes') + def common(self): return self.filter(category=self.model.COMMON) def online(self): return self.filter(category=self.model.ONLINE) + def wines(self): + return self.filter(type__index_name=ProductType.WINE) + + def by_type(self, type: str): + """Filter by type.""" + return self.filter(product_type__index_name=type) + + def by_subtype(self, subtype: str): + """Filter by subtype.""" + return self.filter(subtypes__index_name=subtype) + class Product(TranslatedFieldsMixin, BaseAttributes): """Product models.""" + STR_FIELD_NAME = 'name' + COMMON = 0 ONLINE = 1 @@ -75,10 +123,17 @@ class Product(TranslatedFieldsMixin, BaseAttributes): country = models.ForeignKey('location.Country', on_delete=models.PROTECT, verbose_name=_('Country')) available = models.BooleanField(_('Available'), default=True) - type = models.ForeignKey(ProductType, on_delete=models.PROTECT, - related_name='products', verbose_name=_('Type')) - subtypes = models.ManyToManyField(ProductSubType, related_name='products', + product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, + related_name='products', verbose_name=_('Type')) + subtypes = models.ManyToManyField(ProductSubType, blank=True, + related_name='products', verbose_name=_('Subtypes')) + establishment = models.ForeignKey('establishment.Establishment', + on_delete=models.PROTECT, + related_name='products', + verbose_name=_('establishment')) + public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, + verbose_name=_('public mark'),) objects = ProductManager.from_queryset(ProductQuerySet)() @@ -93,7 +148,7 @@ class OnlineProductManager(ProductManager): """Extended manger for OnlineProduct model.""" def get_queryset(self): - """Overrided get_queryset method.""" + """Overridden get_queryset method.""" return super().get_queryset().online() diff --git a/apps/product/serializers/__init__.py b/apps/product/serializers/__init__.py index e69de29b..c564831e 100644 --- a/apps/product/serializers/__init__.py +++ b/apps/product/serializers/__init__.py @@ -0,0 +1,3 @@ +from .common import * +from .web import * +from .mobile import * diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 236cd38b..f4f02b4d 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -1 +1,55 @@ +"""Product app serializers.""" from rest_framework import serializers +from utils.serializers import TranslatedField +from product.models import Product, ProductSubType, ProductType + + +class ProductSubTypeBaseSerializer(serializers.ModelSerializer): + """ProductSubType base serializer""" + name_translated = TranslatedField() + index_name_display = serializers.CharField(source='get_index_name_display') + + class Meta: + model = ProductSubType + fields = [ + 'id', + 'name_translated', + 'index_name_display', + ] + + +class ProductTypeBaseSerializer(serializers.ModelSerializer): + """ProductType base serializer""" + name_translated = TranslatedField() + index_name_display = serializers.CharField(source='get_index_name_display') + + class Meta: + model = ProductType + fields = [ + 'id', + 'name_translated', + 'index_name_display', + ] + + +class ProductBaseSerializer(serializers.ModelSerializer): + """Product base serializer.""" + name_translated = TranslatedField() + description_translated = TranslatedField() + category_display = serializers.CharField(source='get_category_display') + product_type = ProductTypeBaseSerializer() + subtypes = ProductSubTypeBaseSerializer(many=True) + + class Meta: + """Meta class.""" + model = Product + fields = [ + 'id', + 'name_translated', + 'category_display', + 'description_translated', + 'available', + 'product_type', + 'subtypes', + 'public_mark', + ] diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index e69de29b..57abf4f0 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -0,0 +1,10 @@ +"""Product url patterns.""" +from django.urls import path + +from product import views + +app_name = 'product' + +urlpatterns = [ + path('', views.ProductListView.as_view(), name='list') +] diff --git a/apps/product/urls/web.py b/apps/product/urls/web.py index e69de29b..116c7b0b 100644 --- a/apps/product/urls/web.py +++ b/apps/product/urls/web.py @@ -0,0 +1,7 @@ +"""Product web url patterns.""" +from product.urls.common import urlpatterns as common_urlpatterns + +urlpatterns = [ +] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/product/views/__init__.py b/apps/product/views/__init__.py index e69de29b..6f4a8001 100644 --- a/apps/product/views/__init__.py +++ b/apps/product/views/__init__.py @@ -0,0 +1,4 @@ +from .back import * +from .common import * +from .mobile import * +from .web import * diff --git a/apps/product/views/common.py b/apps/product/views/common.py index e69de29b..403781e4 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -0,0 +1,20 @@ +"""Product app views.""" +from rest_framework import generics, permissions +from product.models import Product +from product import serializers +from product import filters + + +class ProductBaseView(generics.GenericAPIView): + """Product base view""" + + def get_queryset(self): + """Override get_queryset method.""" + return Product.objects.with_base_related() + + +class ProductListView(ProductBaseView, generics.ListAPIView): + """List view for model Product.""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.ProductBaseSerializer + filter_class = filters.ProductListFilterSet diff --git a/project/settings/base.py b/project/settings/base.py index fb542fce..85274993 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -75,6 +75,7 @@ PROJECT_APPS = [ 'favorites.apps.FavoritesConfig', 'rating.apps.RatingConfig', 'tag.apps.TagConfig', + 'product.apps.ProductConfig', ] EXTERNAL_APPS = [ diff --git a/project/urls/web.py b/project/urls/web.py index 77a06961..86f7eac2 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -35,4 +35,5 @@ urlpatterns = [ path('comments/', include('comment.urls.web')), path('favorites/', include('favorites.urls')), path('timetables/', include('timetable.urls.web')), + path('products/', include('product.urls.web')), ]