diff --git a/.idea/Rasaddam_Backend.iml b/.idea/Rasaddam_Backend.iml index c5d6090..168bde0 100644 --- a/.idea/Rasaddam_Backend.iml +++ b/.idea/Rasaddam_Backend.iml @@ -14,7 +14,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 29f5506..296aa57 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 55ca260..474ba12 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -383,8 +383,8 @@ JAZZMIN_SETTINGS = { # Copyright on the footer "copyright": "Acme Library Ltd", - # List of model admins to search from the search bar, search bar omitted if excluded - # If you want to use a single search field you dont need to use a list, you can use a simple string + # List of model admins to filter from the filter bar, filter bar omitted if excluded + # If you want to use a single filter field you dont need to use a list, you can use a simple string "search_model": ["auth.User", "auth.Group"], # Field name on user model that contains avatar ImageField/URLField/Charfield or a callable that receives the user diff --git a/Rasaddam_Backend/urls.py b/Rasaddam_Backend/urls.py index 815ad50..e95c553 100644 --- a/Rasaddam_Backend/urls.py +++ b/Rasaddam_Backend/urls.py @@ -37,7 +37,7 @@ urlpatterns = [ path('herd/', include('apps.herd.urls')), path('livestock/', include('apps.livestock.urls')), path('tag/', include('apps.tag.urls')), - path('search/', include('apps.search.urls')), + path('filter/', include('apps.search.urls')), path('product/', include('apps.product.urls')), path('warehouse/', include('apps.warehouse.urls')), path('pos_device/', include('apps.pos_device.urls')), diff --git a/apps/authentication/api/v1/search_view.py b/apps/authentication/api/v1/search_view.py index 455d79b..30eff73 100644 --- a/apps/authentication/api/v1/search_view.py +++ b/apps/authentication/api/v1/search_view.py @@ -24,8 +24,8 @@ # def get(self, request, query): # try: # q = self.generate_q_expression(query) -# search = self.document_class.search().query(q) -# response = search.execute() +# filter = self.document_class.filter().query(q) +# response = filter.execute() # # print(f"Found {response.hits.total.value} hit(s) for query: '{query}'") # diff --git a/apps/warehouse/services/search/__init__.py b/apps/core/mixins/__init__.py similarity index 100% rename from apps/warehouse/services/search/__init__.py rename to apps/core/mixins/__init__.py diff --git a/apps/core/mixins/search_mixin.py b/apps/core/mixins/search_mixin.py new file mode 100644 index 0000000..60ec2e7 --- /dev/null +++ b/apps/core/mixins/search_mixin.py @@ -0,0 +1,29 @@ +from apps.core.services.filter.search import DynamicSearchService + + +class DynamicSearchMixin: + """ search query sets with introduced fields in view set """ + + def get_search_fields(self): + return getattr(self, "search_fields", []) + + def get_date_field(self): + return getattr(self, "date_field", "create_date") + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) # noqa + + q = self.request.query_params.get("search") # noqa + start = self.request.query_params.get("start") # noqa + end = self.request.query_params.get("end") # noqa + search_fields = self.get_search_fields() + date_field = self.get_date_field() + + return DynamicSearchService( + queryset=queryset, + query_string=q, + search_fields=search_fields, + start=start, + end=end, + date_field=date_field + ).apply() diff --git a/apps/core/services/filter/__init__.py b/apps/core/services/filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/core/services/filter/search.py b/apps/core/services/filter/search.py new file mode 100644 index 0000000..229a335 --- /dev/null +++ b/apps/core/services/filter/search.py @@ -0,0 +1,40 @@ +from django.db.models import Q +from django.db.models.functions import TruncDate + + +class DynamicSearchService: + def __init__(self, queryset, query_string: str = "", search_fields: list[str] = None, + start: str = None, end: str = None, date_field: str = "create_date"): + self.queryset = queryset + self.query = (query_string or "").strip() + self.fields = search_fields or [] + self.start = start + self.end = end + self.date_field = date_field + + def apply(self): + qs = self.queryset + + if self.query and self.fields: + keywords = [k.strip() for k in self.query.split(",") if k.strip()] + q_obj = Q() + for kw in keywords: + for field in self.fields: + q_obj |= Q(**{f"{field}__icontains": kw}) # noqa + qs = qs.filter(q_obj).distinct() + + if self.start or self.end: + qs = qs.annotate(date_only=TruncDate(self.date_field)) + if self.start: + try: + qs = qs.filter(date_only__gte=self.start) + except ValueError: + pass # or log + + if self.end: + try: + qs = qs.filter(date_only__lte=self.end) + except ValueError: + pass # or log + + return qs diff --git a/apps/product/services/search/distribution_search.py b/apps/product/services/search/distribution_search.py deleted file mode 100644 index 33df23b..0000000 --- a/apps/product/services/search/distribution_search.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.db.models import Q -from apps.product.models import QuotaDistribution -from django.db.models.functions import TruncDate - - -class QuotaDistributionSearch: - def __init__(self, query: str = None, start_date: str = None, end_date: str = None): - self.query = (query or '').strip() - self.start_date = start_date - self.end_date = end_date - - def search(self): - """ multi term search & filter by date range """ - - queryset = QuotaDistribution.objects.all() # noqa - - if self.start_date or self.end_date: - queryset = queryset.annotate(date_only=TruncDate('create_date')) - if self.start_date: - queryset = queryset.filter(date_only__gte=self.start_date) - if self.end_date: - queryset = queryset.filter(date_only__lte=self.end_date) - - # convert string to list of words - if self.query: - keywords = [word.strip() for word in self.query.split(',') if word.strip()] - if keywords: - combined_q = Q() - for keyword in keywords: - combined_q |= Q(assigner_organization__name__icontains=keyword) - combined_q |= Q(assigned_organization__name__icontains=keyword) - combined_q |= Q(distribution_id__icontains=keyword) - combined_q |= Q(quota__quota_id__icontains=keyword) - combined_q |= Q(quota__product__name__icontains=keyword) - combined_q |= Q(quota__sale_type__icontains=keyword) - combined_q |= Q(quota__group__icontains=keyword) - - queryset = queryset.filter(combined_q) - - return queryset.distinct() diff --git a/apps/product/services/search/quota_search.py b/apps/product/services/search/quota_search.py deleted file mode 100644 index eeb7bc7..0000000 --- a/apps/product/services/search/quota_search.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db.models import Q -from django.db.models.functions import TruncDate -from apps.product.models import Quota - - -class QuotaSearch: - def __init__(self, query: str = None, start_date: str = None, end_date: str = None): - self.query = (query or '').strip() - self.start_date = start_date - self.end_date = end_date - - def search(self): - """ multi term search & filter by date range """ - - queryset = QuotaDistribution.objects.all() # noqa - - if self.start_date or self.end_date: - queryset = queryset.annotate(date_only=TruncDate('create_date')) - if self.start_date: - queryset = queryset.filter(date_only__gte=self.start_date) - if self.end_date: - queryset = queryset.filter(date_only__lte=self.end_date) - - # convert string to list of words - if self.query: - keywords = [word.strip() for word in self.query.split(',') if word.strip()] - if keywords: - combined_q = Q() - for keyword in keywords: - combined_q |= Q(registerer_organization__name__icontains=keyword) - combined_q |= Q(quota_id__icontains=keyword) - combined_q |= Q(product__name__icontains=keyword) - combined_q |= Q(sale_type__icontains=keyword) - combined_q |= Q(sale_unit__unit__icontains=keyword) - combined_q |= Q(group__icontains=keyword) - - queryset = queryset.filter(combined_q) - - return queryset.distinct() diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index 35d7f7d..55990e4 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -1,8 +1,8 @@ from apps.product.web.api.v1.serializers import quota_distribution_serializers from apps.product.services.services import get_products_in_warehouse from apps.product.web.api.v1.serializers import quota_serializers -from apps.product.services.search.quota_search import QuotaSearch from apps.product.exceptions import QuotaExpiredTimeException +from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.core.pagination import CustomPageNumberPagination from apps.product.web.api.v1.viewsets import product_api from common.helpers import get_organization_by_user @@ -31,31 +31,21 @@ def delete(queryset, pk): obj.delete() -class QuotaViewSet(viewsets.ModelViewSet): # noqa +class QuotaViewSet(viewsets.ModelViewSet, DynamicSearchMixin): # noqa """ apis for product quota """ queryset = product_models.Quota.objects.all() serializer_class = quota_serializers.QuotaSerializer filter_backends = [filters.SearchFilter] CustomPageNumberPagination.page_size = 5 - search_fields = [''] - - def list(self, request, *args, **kwargs): - """ search & filter quotas or return all """ - - params = self.request.query_params - query = params.get('search') - start_date = params.get('start') - end_date = params.get('end') - - search = QuotaSearch( - query=query, - start_date=start_date, - end_date=end_date - ) - - serializer = self.serializer_class(search.search(), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + search_fields = [ + "registerer_organization__name", + "quota_id", + "product__name", + "sale_type", + "sale_unit__unit", + "group", + ] @transaction.atomic def create(self, request, *args, **kwargs): @@ -320,11 +310,13 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa def active_quotas(self, request): """ list of organization active quotas """ + queryset = self.filter_queryset(self.queryset) # return by search param or all objects + organization = get_organization_by_user(request.user) # paginate queryset page = self.paginate_queryset( - self.queryset.filter( + queryset.filter( Q(registerer_organization=organization), Q(is_closed=False) ).order_by('-modify_date') @@ -344,11 +336,13 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa def closed_quotas(self, request): """ list of organization closed quotas """ + queryset = self.filter_queryset(self.queryset) # return by search param or all objects + organization = get_organization_by_user(request.user) # paginate queryset page = self.paginate_queryset( - self.queryset.filter( + queryset.filter( Q(registerer_organization=organization), Q(is_closed=True) ).order_by('-modify_date') @@ -369,10 +363,13 @@ class QuotaViewSet(viewsets.ModelViewSet): # noqa try: quota = self.get_object() + queryset = self.filter_queryset( + quota.distributions_assigned.all().order_by('-modify_date') + ) # return by search param or all objects # paginate queryset page = self.paginate_queryset( - quota.distributions_assigned.all().order_by('-modify_date') + queryset ) if page is not None: serializer = quota_distribution_serializers.QuotaDistributionSerializer( diff --git a/apps/product/web/api/v1/viewsets/quota_distribution_api.py b/apps/product/web/api/v1/viewsets/quota_distribution_api.py index d72fc2b..379d07e 100644 --- a/apps/product/web/api/v1/viewsets/quota_distribution_api.py +++ b/apps/product/web/api/v1/viewsets/quota_distribution_api.py @@ -1,7 +1,5 @@ -from django.db.models import Q - from apps.product.web.api.v1.serializers import quota_distribution_serializers as distribution_serializers -from apps.product.services.search.distribution_search import QuotaDistributionSearch +from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.core.pagination import CustomPageNumberPagination from rest_framework.exceptions import APIException from apps.product import models as product_models @@ -10,6 +8,7 @@ from rest_framework.decorators import action from rest_framework import viewsets, filters from rest_framework import status from django.db import transaction +from django.db.models import Q from common.helpers import get_organization_by_user @@ -27,31 +26,23 @@ def delete(queryset, pk): obj.delete() -class QuotaDistributionViewSet(viewsets.ModelViewSet): +class QuotaDistributionViewSet(viewsets.ModelViewSet, DynamicSearchMixin): """ quota distribution apis """ queryset = product_models.QuotaDistribution.objects.all() serializer_class = distribution_serializers.QuotaDistributionSerializer filter_backends = [filters.SearchFilter] - search_fields = [''] CustomPageNumberPagination.page_size = 5 - - def list(self, request, *args, **kwargs): - """ search & filter in distributions or return all objects """ - - params = self.request.query_params - query = params.get('search') - start_date = params.get('start') - end_date = params.get('end') - - search = QuotaDistributionSearch( - query, - start_date, - end_date - ) - - serializer = self.serializer_class(search.search(), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + search_fields = [ + "assigner_organization__name", + "assigned_organization__name", + "distribution_id", + "quota__quota_id", + "quota__product__name", + "quota__sale_type", + "quota__group", + ] + date_field = "create_date" @transaction.atomic def create(self, request, *args, **kwargs): @@ -99,13 +90,15 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet): ) def my_distributions(self, request): """ list of my distributions """ - query = self.request.query_params + + queryset = self.filter_queryset(self.queryset) # return by search param or all objects organization = get_organization_by_user(request.user) + query = self.request.query_params if query.get('param') == 'assigned': # paginate queryset page = self.paginate_queryset( - self.queryset.filter( + queryset.filter( Q(assigned_organization=organization) ).order_by('-modify_date') ) @@ -113,7 +106,7 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet): elif query.get('param') == 'assigner': # paginate queryset page = self.paginate_queryset( - self.queryset.filter( + queryset.filter( Q(assigner_organization=organization) ).order_by('-modify_date') ) @@ -121,7 +114,7 @@ class QuotaDistributionViewSet(viewsets.ModelViewSet): elif query.get('param') == 'all': # paginate queryset page = self.paginate_queryset( - self.queryset.filter( + queryset.filter( Q(assigner_organization=organization) | Q(assigned_organization=organization) ).order_by('-modify_date') diff --git a/apps/search/document/user_document.py b/apps/search/document/user_document.py index f83ca79..e337d2c 100644 --- a/apps/search/document/user_document.py +++ b/apps/search/document/user_document.py @@ -19,7 +19,7 @@ class UserRelationDocument(Document): # In different parts of the code different fields are used. There are # a couple of use cases: (1) more-like-this functionality, where `title`, - # `description` and `summary` fields are used, (2) search and filtering + # `description` and `summary` fields are used, (2) filter and filtering # functionality where all the fields are used. user = fields.ObjectField(properties={ diff --git a/apps/warehouse/services/search/inventory_search.py b/apps/warehouse/services/search/inventory_search.py deleted file mode 100644 index a129dee..0000000 --- a/apps/warehouse/services/search/inventory_search.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.db.models import Q -from apps.warehouse.models import InventoryEntry -from django.db.models.functions import TruncDate - - -class InventoryEntrySearch: - def __init__(self, query: str = None, start_date: str = None, end_date: str = None): - self.query = (query or '').strip() - self.start_date = start_date - self.end_date = end_date - - def search(self): - """ multi term search & filter by date range """ - - queryset = InventoryEntry.objects.all() - - if self.start_date or self.end_date: - queryset = queryset.annotate(date_only=TruncDate('create_date')) - if self.start_date: - queryset = queryset.filter(date_only__gte=self.start_date) - if self.end_date: - queryset = queryset.filter(date_only__lte=self.end_date) - - # convert string to list of words - if self.query: - keywords = [word.strip() for word in self.query.split(',') if word.strip()] - if keywords: - combined_q = Q() - for keyword in keywords: - combined_q |= Q(distribution__distribution_id__icontains=keyword) - combined_q |= Q(organization__name__icontains=keyword) - combined_q |= Q(weight__icontains=keyword) - combined_q |= Q(balance__icontains=keyword) - combined_q |= Q(lading_number__icontains=keyword) - combined_q |= Q(is_confirmed__icontains=keyword) - - queryset = queryset.filter(combined_q) - - return queryset.distinct() diff --git a/apps/warehouse/web/api/v1/api.py b/apps/warehouse/web/api/v1/api.py index 7098ce5..79d253c 100644 --- a/apps/warehouse/web/api/v1/api.py +++ b/apps/warehouse/web/api/v1/api.py @@ -1,5 +1,5 @@ from apps.warehouse.web.api.v1 import serializers as warehouse_serializers -from apps.warehouse.services.search.inventory_search import InventoryEntrySearch +from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.warehouse import models as warehouse_models from common.helpers import get_organization_by_user from common.generics import base64_to_image_file @@ -12,11 +12,19 @@ from rest_framework import status import typing -class InventoryEntryViewSet(viewsets.ModelViewSet): +class InventoryEntryViewSet(viewsets.ModelViewSet, DynamicSearchMixin): queryset = warehouse_models.InventoryEntry.objects.all() serializer_class = warehouse_serializers.InventoryEntrySerializer filter_backends = [filters.SearchFilter] - search_fields = [''] + search_fields = [ + "distribution__distribution_id", + "organization__name", + "weight", + "balance", + "lading_number", + "is_confirmed", + ] + date_field = "create_date" def upload_confirmation_document(self, request, inventory: int) -> typing.Any: """ upload document for inventory entry confirmation """ @@ -40,24 +48,6 @@ class InventoryEntryViewSet(viewsets.ModelViewSet): inventory.save() return Response(status=status.HTTP_200_OK) - @transaction.atomic - def list(self, request, *args, **kwargs): - """ search & filter by date range or return all objects """ - - query_params = self.request.query_params - query = query_params.get('search') - start_date = query_params.get('start') - end_date = query_params.get('end') - - search = InventoryEntrySearch( - query, - start_date, - end_date - ) - - serializer = self.serializer_class(search.search(), many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - @transaction.atomic def create(self, request, *args, **kwargs): """ custom create of inventory entry """ @@ -101,7 +91,8 @@ class InventoryEntryViewSet(viewsets.ModelViewSet): def my_inventory_entries(self, request): """ list of my inventory entries """ - entries = self.queryset.filter(organization=get_organization_by_user(request.user)) + queryset = self.filter_queryset(self.queryset) # return by search param or all objects + entries = queryset.filter(organization=get_organization_by_user(request.user)) # paginate & response page = self.paginate_queryset(entries)