From fc3b0af097837add2213813f78a8fbc814aef651 Mon Sep 17 00:00:00 2001 From: Ruslan Atarov Date: Tue, 5 Dec 2023 23:16:21 +0300 Subject: [PATCH 1/2] fixed filters and dealer_keys lists --- apps/api/v1/dealers/filters.py | 18 +++++++++++++++--- apps/api/v1/dealers/schema.py | 7 +++++-- apps/api/v1/dealers/serializer.py | 19 +++++++++++-------- apps/api/v1/prices/views.py | 4 +++- apps/dealers/crud.py | 26 ++++++++++++-------------- apps/dealers/services.py | 6 ++++-- apps/prices/crud.py | 5 +++++ apps/prices/services.py | 2 +- apps/services/match_service.py | 4 +++- config/constants.py | 14 +++++++++++++- 10 files changed, 72 insertions(+), 33 deletions(-) diff --git a/apps/api/v1/dealers/filters.py b/apps/api/v1/dealers/filters.py index 2424aab..c64c739 100644 --- a/apps/api/v1/dealers/filters.py +++ b/apps/api/v1/dealers/filters.py @@ -2,11 +2,13 @@ from django.db.models import Q from apps.dealers.models import DealerKey +from config.constants import KeyStatus, MATCH_NUMBER class DealerKeyFilter(filters.FilterSet): article = filters.CharFilter(method="get_article") status = filters.CharFilter(method="get_status") + # similarity = filters.NumberFilter(method="get_similarity") class Meta: model = DealerKey @@ -24,6 +26,16 @@ def get_status(self, queryset, name, value): """Поиск по статусу.""" if not value: return queryset - if value == "-": - return queryset.filter(status=101) - return queryset.exclude(status=101) + if value == KeyStatus.FOUND: + return queryset.filter(product_id__isnull=False) + if value == KeyStatus.DECLINED: + return queryset.filter(declined=MATCH_NUMBER) + return queryset.filter( + product_id__isnull=True, declined__lt=MATCH_NUMBER + ) + + # def get_similarity(self, queryset, name, value): + # """Поиск по показателю схожести.""" + # if value: + # return queryset.filter(similarity__gte=value) + # return queryset diff --git a/apps/api/v1/dealers/schema.py b/apps/api/v1/dealers/schema.py index 4b1ddaa..0f21ec4 100644 --- a/apps/api/v1/dealers/schema.py +++ b/apps/api/v1/dealers/schema.py @@ -1,7 +1,8 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter -from . import serializer as ser +from apps.dealers.models import Match +from . import serializer as ser dealer_schema = { "list": extend_schema(description="Получение списка дилеров."), @@ -9,6 +10,8 @@ } +filter_options = Match.MatchStatus.choices + key_schema = { "list": extend_schema( description="Получение списка уникальных ключей дилеров.", @@ -23,7 +26,7 @@ ), OpenApiParameter( name="status", - description="Фильтр по статусу; варианты: '-', '0', '1+'", + description=f"Фильтр по статусу: {filter_options}", ), OpenApiParameter( name="page", diff --git a/apps/api/v1/dealers/serializer.py b/apps/api/v1/dealers/serializer.py index 573268a..d17f78c 100644 --- a/apps/api/v1/dealers/serializer.py +++ b/apps/api/v1/dealers/serializer.py @@ -2,6 +2,7 @@ from apps.dealers.models import Dealer, DealerKey, Match from apps.products.crud import product_exists +from config.constants import MATCH_NUMBER, KeyStatus from ..products.serializer import ProductShortSerializer @@ -53,8 +54,8 @@ class KeySerializer(serializers.ModelSerializer): product = ProductShortSerializer() name = serializers.CharField() last_price = serializers.DecimalField(max_digits=7, decimal_places=2) - similarity = serializers.SerializerMethodField() - # status = serializers.IntegerField() + # similarity = serializers.IntegerField() + status = serializers.SerializerMethodField() class Meta: model = DealerKey @@ -63,16 +64,18 @@ class Meta: "key", "name", "last_price", - # "status", - "similarity", + "status", + # "similarity", "dealer", "product", ) - def get_similarity(self, obj): - if obj.status == 101: - return "-" - return obj.status + def get_status(self, obj): + if obj.product_id is not None: + return KeyStatus.FOUND + if obj.declined == MATCH_NUMBER: + return KeyStatus.DECLINED + return KeyStatus.CHECK class MatchSerializer(serializers.ModelSerializer): diff --git a/apps/api/v1/prices/views.py b/apps/api/v1/prices/views.py index 3fd99a4..7fa39e1 100644 --- a/apps/api/v1/prices/views.py +++ b/apps/api/v1/prices/views.py @@ -2,7 +2,7 @@ from rest_framework import views, status from rest_framework.response import Response -from apps.prices.crud import list_key_prices +from apps.prices.crud import list_key_prices, there_are_prices_in_db from apps.prices.services import delete_prices_and_relations, create_prices from ..pagination import NestedPagePagination @@ -24,6 +24,8 @@ class PricesView(views.APIView): """Загрузка и удаление цен дилеров и связанных ключей дилеров.""" def post(self, request): + if there_are_prices_in_db(): + return Response(status=status.HTTP_400_BAD_REQUEST) create_prices() return Response(status=status.HTTP_201_CREATED) diff --git a/apps/dealers/crud.py b/apps/dealers/crud.py index bb86dc5..0bd52c2 100644 --- a/apps/dealers/crud.py +++ b/apps/dealers/crud.py @@ -2,8 +2,6 @@ QuerySet, OuterRef, Subquery, - Case, - When, Value, Count, Q, @@ -39,7 +37,10 @@ def list_dealers_report_data() -> QuerySet[Dealer]: ), ), # TODO добавить код, когда будут модели рекомендаций - confirmed_matches=Value(1), + confirmed_matches=Count( + "dealer_keys__matches", + filter=Q(dealer_keys__matches__status=Match.MatchStatus.YES), + ), to_be_checked=Value(1), no_matches=Value(1), ) @@ -59,15 +60,14 @@ def list_keys() -> QuerySet[DealerKey]: "price" )[:1] ), - status=Case( - When(product__isnull=False, then=Value(101)), - # TODO код для расчета статуса по рекомендациям - default=Subquery( - Match.objects.filter(key_id=OuterRef("pk")).values( - "similarity" - )[:1] - ), + declined=Count( + "matches", filter=Q(matches__status=Match.MatchStatus.NO) ), + # similarity=Subquery( + # Match.objects.filter(key_id=OuterRef("pk")).values( + # "similarity" + # )[:1] + # ), ) # фильтр позволяет выгружать только ключи, которые есть в списке цен .filter(name__isnull=False) @@ -76,9 +76,7 @@ def list_keys() -> QuerySet[DealerKey]: def list_matches(key_pk: int, add_products: bool = True) -> QuerySet[Match]: """Получение списка возможных соответствий Ключ - Продукт.""" - subquery = Match.objects.filter(key_id=key_pk) - if add_products: - subquery = subquery.select_related("product") + subquery = Match.objects.filter(key_id=key_pk).select_related("product") query = DealerKey.objects.prefetch_related( Prefetch( "matches", diff --git a/apps/dealers/services.py b/apps/dealers/services.py index 5c666fd..dbe00ef 100644 --- a/apps/dealers/services.py +++ b/apps/dealers/services.py @@ -12,14 +12,16 @@ @atomic def decline_matches(key_pk: int) -> QuerySet[Match]: - matches = list_matches(key_pk=key_pk, add_products=False) + """Сменить всем предложениям статус на "Не подходит".""" + matches = list_matches(key_pk=key_pk) change_status_to_declined(matches=matches) return matches @atomic def choose_match(key_pk: int, product_id: int) -> QuerySet[Match]: - matches = list_matches(key_pk=key_pk, add_products=False) + """Выбрать продукт из списка предложений.""" + matches = list_matches(key_pk=key_pk) choose_one_decline_others(matches=matches, product_id=product_id) set_product_for_dealer_key(dealer_key_id=key_pk, product_id=product_id) return matches diff --git a/apps/prices/crud.py b/apps/prices/crud.py index d4c3a69..3fd4436 100644 --- a/apps/prices/crud.py +++ b/apps/prices/crud.py @@ -20,6 +20,11 @@ def list_prices() -> QuerySet[DealerPrice]: ) +def there_are_prices_in_db() -> bool: + """Проверяет наличие в БД загруженных цен.""" + return DealerPrice.objects.exists() + + def delete_all_prices() -> None: """Удаление всех цен.""" DealerPrice.objects.all().delete() diff --git a/apps/prices/services.py b/apps/prices/services.py index f8bf82e..fea8998 100644 --- a/apps/prices/services.py +++ b/apps/prices/services.py @@ -70,7 +70,6 @@ def create_prices() -> None: dealer_id=dataset["dealer_id"], id=id_counter, ) - fields = form_price_obj_fields(id=dealer_key.id, dataset=dataset) if created: id_counter += 1 @@ -78,6 +77,7 @@ def create_prices() -> None: elif dealer_key.id in keys_to_match: keys_to_match[dealer_key.id] = dataset["product_name"] + fields = form_price_obj_fields(id=dealer_key.id, dataset=dataset) prices_datasets.append(fields) recommend_products(key_datasets=keys_to_match) prices_bulk_create(fields_sets=prices_datasets) diff --git a/apps/services/match_service.py b/apps/services/match_service.py index b02d5ee..81405c2 100644 --- a/apps/services/match_service.py +++ b/apps/services/match_service.py @@ -4,6 +4,8 @@ from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics import pairwise_distances +from config.constants import MATCH_NUMBER + class RecommendationService: def __init__( @@ -94,7 +96,7 @@ def _matching_names( return df def get_recommendations( - self, dealer_name: str, recommendations_number: int = 10 + self, dealer_name: str, recommendations_number: int = MATCH_NUMBER ) -> list[list[float, float]]: """Получение рекомендаций.""" # получаем ключи по названию diff --git a/config/constants.py b/config/constants.py index fe262bf..de48944 100644 --- a/config/constants.py +++ b/config/constants.py @@ -1,4 +1,16 @@ +from dataclasses import dataclass + # Настройки общей пагинации COMMON_PAGE = 50 # Настройки вложенной в объект пагинации (история цен) -NESTED_PAGE = 3 +NESTED_PAGE = 10 +# Кол-во выводимых предложений аналогов +MATCH_NUMBER = 10 + + +# Статусы ключей дилеров +@dataclass +class KeyStatus: + CHECK = "На проверку" + DECLINED = "Не подходит" + FOUND = "Подтверждено" From ee8d457197d2ef2e4c22af5f611c5d3ea0d82a86 Mon Sep 17 00:00:00 2001 From: Ruslan Atarov Date: Tue, 5 Dec 2023 23:46:55 +0300 Subject: [PATCH 2/2] finished report calculations --- apps/api/v1/dealers/schema.py | 4 ++-- apps/api/v1/dealers/serializer.py | 5 ++++- apps/dealers/crud.py | 12 ++++++----- apps/prices/migrations/0002_load_prices.py | 24 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 apps/prices/migrations/0002_load_prices.py diff --git a/apps/api/v1/dealers/schema.py b/apps/api/v1/dealers/schema.py index 0f21ec4..7dfcef9 100644 --- a/apps/api/v1/dealers/schema.py +++ b/apps/api/v1/dealers/schema.py @@ -1,6 +1,6 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter -from apps.dealers.models import Match +from config.constants import KeyStatus from . import serializer as ser @@ -10,7 +10,7 @@ } -filter_options = Match.MatchStatus.choices +filter_options = [KeyStatus.CHECK, KeyStatus.DECLINED, KeyStatus.FOUND] key_schema = { "list": extend_schema( diff --git a/apps/api/v1/dealers/serializer.py b/apps/api/v1/dealers/serializer.py index d17f78c..cc1e4e9 100644 --- a/apps/api/v1/dealers/serializer.py +++ b/apps/api/v1/dealers/serializer.py @@ -30,7 +30,7 @@ class DealerReportSerializer(BaseDealerSerializer): keys_without_product = serializers.SerializerMethodField() confirmed_matches = serializers.IntegerField() to_be_checked = serializers.IntegerField() - no_matches = serializers.IntegerField() + no_matches = serializers.SerializerMethodField() class Meta(BaseDealerSerializer.Meta): fields = BaseDealerSerializer.Meta.fields + ( @@ -46,6 +46,9 @@ class Meta(BaseDealerSerializer.Meta): def get_keys_without_product(self, obj): return obj.total_keys - obj.keys_with_product + def get_no_matches(self, obj): + return obj.total_keys - obj.keys_with_product - obj.to_be_checked + class KeySerializer(serializers.ModelSerializer): """Сериализатор для полей Ключей/артикулов Дилера.""" diff --git a/apps/dealers/crud.py b/apps/dealers/crud.py index 0bd52c2..0088ae3 100644 --- a/apps/dealers/crud.py +++ b/apps/dealers/crud.py @@ -2,7 +2,6 @@ QuerySet, OuterRef, Subquery, - Value, Count, Q, Prefetch, @@ -36,13 +35,16 @@ def list_dealers_report_data() -> QuerySet[Dealer]: & Q(dealer_keys__prices__isnull=False) ), ), - # TODO добавить код, когда будут модели рекомендаций confirmed_matches=Count( - "dealer_keys__matches", + "dealer_keys", + distinct=True, filter=Q(dealer_keys__matches__status=Match.MatchStatus.YES), ), - to_be_checked=Value(1), - no_matches=Value(1), + to_be_checked=Count( + "dealer_keys", + distinct=True, + filter=Q(dealer_keys__matches__status=Match.MatchStatus.NEW), + ), ) diff --git a/apps/prices/migrations/0002_load_prices.py b/apps/prices/migrations/0002_load_prices.py new file mode 100644 index 0000000..29df688 --- /dev/null +++ b/apps/prices/migrations/0002_load_prices.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2023-11-29 13:20 + +from django.db import migrations, models + +from apps.services.fixtures_parser import get_prices_datasets + + +def create_prices(apps, schema_editor): + DealerPrice: models.Model = apps.get_model("prices", "DealerPrice") + prices_datasets = get_prices_datasets() + if prices_datasets: + prices = [DealerPrice(**fields) for fields in prices_datasets] + DealerPrice.objects.bulk_create(prices, ignore_conflicts=True) + + +class Migration(migrations.Migration): + dependencies = [ + ("prices", "0001_initial"), + ("dealers", "0003_dealerkey_unique_pair_dealer_and_key"), + ] + + operations = [ + migrations.RunPython(create_prices), + ]