All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 16s
175 lines
7.7 KiB
Python
175 lines
7.7 KiB
Python
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from django.db import connection
|
|
from .typesense_client import TypesenseService
|
|
import logging
|
|
import time
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class ProductSearchView(APIView):
|
|
def get(self, request):
|
|
try:
|
|
q = request.query_params.get('q', '*').strip()
|
|
category = request.query_params.get('category', '').strip()
|
|
min_price = request.query_params.get('min_price')
|
|
max_price = request.query_params.get('max_price')
|
|
is_gi_tagged = request.query_params.get('is_gi_tagged')
|
|
supplier_id = request.query_params.get('supplier_id')
|
|
sort_by_param = request.query_params.get('sort_by', '').strip()
|
|
|
|
try:
|
|
page = max(1, int(request.query_params.get('page', 1)))
|
|
per_page = min(250, max(1, int(request.query_params.get('per_page', 20))))
|
|
except (ValueError, TypeError):
|
|
return Response({'error': 'Invalid page or per_page parameter'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
filter_clauses = ['is_active:=true']
|
|
|
|
if category:
|
|
escaped_category = category.replace('"', '\\"')
|
|
filter_clauses.append(f'category:="{escaped_category}"')
|
|
|
|
if supplier_id:
|
|
try:
|
|
s_id = int(supplier_id)
|
|
filter_clauses.append(f'supplier_id:={s_id}')
|
|
except (ValueError, TypeError):
|
|
return Response({'error': 'supplier_id must be an integer'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if is_gi_tagged is not None:
|
|
val = is_gi_tagged.strip().lower()
|
|
if val in ('true', '1'):
|
|
filter_clauses.append('is_gi_tagged:=true')
|
|
elif val in ('false', '0'):
|
|
filter_clauses.append('is_gi_tagged:=false')
|
|
|
|
if min_price is not None or max_price is not None:
|
|
try:
|
|
p_min = float(min_price) if min_price else 0.0
|
|
p_max = float(max_price) if max_price else 9999999.0
|
|
if p_min > p_max:
|
|
p_min, p_max = p_max, p_min
|
|
filter_clauses.append(f'price:[{p_min}..{p_max}]')
|
|
except (ValueError, TypeError):
|
|
return Response({'error': 'Price filters must be valid numbers'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
filter_by_str = ' && '.join(filter_clauses)
|
|
|
|
if sort_by_param == 'price_asc':
|
|
sort_by_str = 'price:asc'
|
|
elif sort_by_param == 'price_desc':
|
|
sort_by_str = 'price:desc'
|
|
elif sort_by_param == 'relevance':
|
|
sort_by_str = '_text_match:desc,created_at:desc'
|
|
elif sort_by_param == 'newest':
|
|
sort_by_str = 'created_at:desc'
|
|
else:
|
|
sort_by_str = '_text_match:desc,created_at:desc' if (q and q != '*') else 'created_at:desc'
|
|
|
|
res = TypesenseService.search_products(
|
|
query=q,
|
|
filter_by=filter_by_str,
|
|
sort_by=sort_by_str,
|
|
page=page,
|
|
per_page=per_page
|
|
)
|
|
|
|
hits = res.get('hits', [])
|
|
found = res.get('found', 0)
|
|
total_pages = (found + per_page - 1) // per_page if per_page > 0 else 1
|
|
|
|
results = [hit.get('document', {}) for hit in hits]
|
|
facet_counts = res.get('facet_counts', [])
|
|
|
|
return Response({
|
|
'count': found,
|
|
'page': page,
|
|
'per_page': per_page,
|
|
'total_pages': total_pages,
|
|
'facets': facet_counts,
|
|
'results': results
|
|
}, status=status.HTTP_200_OK)
|
|
except Exception as e:
|
|
logger.error(f"Typesense Search Error: {e}", exc_info=True)
|
|
return Response({'error': str(e), 'results': [], 'count': 0}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class IndexProductView(APIView):
|
|
def post(self, request):
|
|
try:
|
|
data = request.data
|
|
if not data:
|
|
return Response({'error': 'Payload cannot be empty'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if isinstance(data, list):
|
|
valid_items = []
|
|
for item in data:
|
|
if isinstance(item, dict) and ('id' in item or 'sku' in item):
|
|
valid_items.append(item)
|
|
if not valid_items:
|
|
return Response({'error': 'Each product in list must have an id or sku'}, status=status.HTTP_400_BAD_REQUEST)
|
|
res = TypesenseService.index_products_batch(valid_items)
|
|
return Response({'status': 'indexed', 'count': len(valid_items), 'result': res}, status=status.HTTP_200_OK)
|
|
elif isinstance(data, dict):
|
|
if 'id' not in data and 'sku' not in data:
|
|
return Response({'error': 'Product id or sku is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
res = TypesenseService.index_product(data)
|
|
return Response({'status': 'indexed', 'result': res}, status=status.HTTP_200_OK)
|
|
else:
|
|
return Response({'error': 'Invalid JSON format. Expected JSON object or array.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
except Exception as e:
|
|
logger.error(f"Product Index Error: {e}", exc_info=True)
|
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class DeleteProductView(APIView):
|
|
def delete(self, request, product_id):
|
|
try:
|
|
if not product_id or not str(product_id).strip():
|
|
return Response({'error': 'Product identifier is required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
res = TypesenseService.delete_product(product_id)
|
|
return Response({'status': 'deleted', 'result': res}, status=status.HTTP_200_OK)
|
|
except Exception as e:
|
|
logger.error(f"Product Delete Error: {e}", exc_info=True)
|
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class ReindexCatalogView(APIView):
|
|
def post(self, request):
|
|
try:
|
|
TypesenseService.ensure_schema(force=True)
|
|
indexed_count = 0
|
|
batch_size = 250
|
|
batch = []
|
|
|
|
with connection.cursor() as cursor:
|
|
cursor.execute("""
|
|
SELECT id, sku, title, description, category, supplier_id, price,
|
|
compare_at_price, stock, image, is_gi_tagged, is_active, status
|
|
FROM api_product
|
|
""")
|
|
columns = [col[0] for col in cursor.description]
|
|
rows = cursor.fetchall()
|
|
now_ts = int(time.time())
|
|
|
|
for idx, row in enumerate(rows):
|
|
item = dict(zip(columns, row))
|
|
# Retain descending ordering with timestamp offset
|
|
item['created_at'] = now_ts + idx
|
|
batch.append(item)
|
|
|
|
if len(batch) >= batch_size:
|
|
TypesenseService.index_products_batch(batch)
|
|
indexed_count += len(batch)
|
|
batch = []
|
|
|
|
if batch:
|
|
TypesenseService.index_products_batch(batch)
|
|
indexed_count += len(batch)
|
|
|
|
return Response({'status': 'completed', 'total_indexed': indexed_count}, status=status.HTTP_200_OK)
|
|
except Exception as e:
|
|
logger.error(f"Full Reindex Error: {e}", exc_info=True)
|
|
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|