views.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. from rest_framework.views import APIView
  2. from rest_framework.response import Response
  3. from rest_framework import status
  4. from rest_framework.parsers import MultiPartParser, FormParser
  5. from django.db import transaction
  6. import pandas as pd
  7. from .models import Product, ProductType, ProductAttribute, AttributePossibleValue
  8. from .serializers import (
  9. SingleProductRequestSerializer,
  10. BatchProductRequestSerializer,
  11. ProductAttributeResultSerializer,
  12. BatchProductResponseSerializer,
  13. ProductSerializer,
  14. ProductTypeSerializer,
  15. ProductAttributeSerializer,
  16. AttributePossibleValueSerializer
  17. )
  18. from .services import ProductAttributeService
  19. from .ocr_service import OCRService
  20. class ExtractProductAttributesView(APIView):
  21. """
  22. API endpoint to extract product attributes for a single product by item_id.
  23. Fetches product details from database with source tracking.
  24. Returns attributes in array format: [{"value": "...", "source": "..."}]
  25. """
  26. def post(self, request):
  27. serializer = SingleProductRequestSerializer(data=request.data)
  28. if not serializer.is_valid():
  29. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  30. validated_data = serializer.validated_data
  31. item_id = validated_data.get("item_id")
  32. # Fetch product from DB
  33. try:
  34. product = Product.objects.get(item_id=item_id)
  35. except Product.DoesNotExist:
  36. return Response(
  37. {"error": f"Product with item_id '{item_id}' not found."},
  38. status=status.HTTP_404_NOT_FOUND
  39. )
  40. # Extract product details
  41. title = product.product_name
  42. short_desc = product.product_short_description
  43. long_desc = product.product_long_description
  44. image_url = product.image_path
  45. # Process image for OCR if required
  46. ocr_results = None
  47. ocr_text = None
  48. if validated_data.get("process_image", True) and image_url:
  49. ocr_service = OCRService()
  50. ocr_results = ocr_service.process_image(image_url)
  51. if ocr_results and ocr_results.get("detected_text"):
  52. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  53. ocr_results, validated_data.get("model")
  54. )
  55. ocr_results["extracted_attributes"] = ocr_attrs
  56. ocr_text = "\n".join([
  57. f"{item['text']} (confidence: {item['confidence']:.2f})"
  58. for item in ocr_results["detected_text"]
  59. ])
  60. # Combine all product text with source tracking
  61. product_text, source_map = ProductAttributeService.combine_product_text(
  62. title=title,
  63. short_desc=short_desc,
  64. long_desc=long_desc,
  65. ocr_text=ocr_text
  66. )
  67. # Extract attributes with enhanced features and source tracking
  68. result = ProductAttributeService.extract_attributes(
  69. product_text=product_text,
  70. mandatory_attrs=validated_data["mandatory_attrs"],
  71. source_map=source_map,
  72. model=validated_data.get("model"),
  73. extract_additional=validated_data.get("extract_additional", True),
  74. multiple=validated_data.get("multiple", []),
  75. threshold_abs=validated_data.get("threshold_abs", 0.65),
  76. margin=validated_data.get("margin", 0.15),
  77. use_dynamic_thresholds=validated_data.get("use_dynamic_thresholds", True),
  78. use_adaptive_margin=validated_data.get("use_adaptive_margin", True),
  79. use_semantic_clustering=validated_data.get("use_semantic_clustering", True)
  80. )
  81. # Attach OCR results if available
  82. if ocr_results:
  83. result["ocr_results"] = ocr_results
  84. response_serializer = ProductAttributeResultSerializer(data=result)
  85. if response_serializer.is_valid():
  86. return Response(response_serializer.data, status=status.HTTP_200_OK)
  87. return Response(result, status=status.HTTP_200_OK)
  88. class BatchExtractProductAttributesView(APIView):
  89. """
  90. API endpoint to extract product attributes for multiple products in batch.
  91. Uses item-specific mandatory_attrs with source tracking.
  92. Returns attributes in array format: [{"value": "...", "source": "..."}]
  93. """
  94. def post(self, request):
  95. serializer = BatchProductRequestSerializer(data=request.data)
  96. if not serializer.is_valid():
  97. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  98. validated_data = serializer.validated_data
  99. # DEBUG: Print what we received
  100. print("\n" + "="*80)
  101. print("BATCH REQUEST - RECEIVED DATA")
  102. print("="*80)
  103. print(f"Raw request data keys: {request.data.keys()}")
  104. print(f"Multiple field in request: {request.data.get('multiple')}")
  105. print(f"Validated multiple field: {validated_data.get('multiple')}")
  106. print("="*80 + "\n")
  107. # Get batch-level settings
  108. product_list = validated_data.get("products", [])
  109. model = validated_data.get("model")
  110. extract_additional = validated_data.get("extract_additional", True)
  111. process_image = validated_data.get("process_image", True)
  112. multiple = validated_data.get("multiple", [])
  113. threshold_abs = validated_data.get("threshold_abs", 0.65)
  114. margin = validated_data.get("margin", 0.15)
  115. use_dynamic_thresholds = validated_data.get("use_dynamic_thresholds", True)
  116. use_adaptive_margin = validated_data.get("use_adaptive_margin", True)
  117. use_semantic_clustering = validated_data.get("use_semantic_clustering", True)
  118. # DEBUG: Print extracted settings
  119. print(f"Extracted multiple parameter: {multiple}")
  120. print(f"Type: {type(multiple)}")
  121. # Extract all item_ids to query the database efficiently
  122. item_ids = [p['item_id'] for p in product_list]
  123. # Fetch all products in one query
  124. products_queryset = Product.objects.filter(item_id__in=item_ids)
  125. # Create a dictionary for easy lookup: item_id -> Product object
  126. product_map = {product.item_id: product for product in products_queryset}
  127. found_ids = set(product_map.keys())
  128. results = []
  129. successful = 0
  130. failed = 0
  131. for product_entry in product_list:
  132. item_id = product_entry['item_id']
  133. # Get item-specific mandatory attributes
  134. mandatory_attrs = product_entry['mandatory_attrs']
  135. if item_id not in found_ids:
  136. failed += 1
  137. results.append({
  138. "product_id": item_id,
  139. "error": "Product not found in database"
  140. })
  141. continue
  142. product = product_map[item_id]
  143. try:
  144. title = product.product_name
  145. short_desc = product.product_short_description
  146. long_desc = product.product_long_description
  147. image_url = product.image_path
  148. ocr_results = None
  149. ocr_text = None
  150. # Image Processing Logic
  151. if process_image and image_url:
  152. ocr_service = OCRService()
  153. ocr_results = ocr_service.process_image(image_url)
  154. if ocr_results and ocr_results.get("detected_text"):
  155. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  156. ocr_results, model
  157. )
  158. ocr_results["extracted_attributes"] = ocr_attrs
  159. ocr_text = "\n".join([
  160. f"{item['text']} (confidence: {item['confidence']:.2f})"
  161. for item in ocr_results["detected_text"]
  162. ])
  163. # Combine product text with source tracking
  164. product_text, source_map = ProductAttributeService.combine_product_text(
  165. title=title,
  166. short_desc=short_desc,
  167. long_desc=long_desc,
  168. ocr_text=ocr_text
  169. )
  170. # DEBUG: Print before extraction
  171. print(f"\n>>> Extracting for product {item_id}")
  172. print(f" Passing multiple: {multiple}")
  173. # Attribute Extraction with source tracking (returns array format)
  174. extracted = ProductAttributeService.extract_attributes(
  175. product_text=product_text,
  176. mandatory_attrs=mandatory_attrs,
  177. source_map=source_map,
  178. model=model,
  179. extract_additional=extract_additional,
  180. multiple=multiple, # Make sure this is passed!
  181. threshold_abs=threshold_abs,
  182. margin=margin,
  183. use_dynamic_thresholds=use_dynamic_thresholds,
  184. use_adaptive_margin=use_adaptive_margin,
  185. use_semantic_clustering=use_semantic_clustering
  186. )
  187. result = {
  188. "product_id": product.item_id,
  189. "mandatory": extracted.get("mandatory", {}),
  190. "additional": extracted.get("additional", {}),
  191. }
  192. if ocr_results:
  193. result["ocr_results"] = ocr_results
  194. results.append(result)
  195. successful += 1
  196. except Exception as e:
  197. failed += 1
  198. results.append({
  199. "product_id": item_id,
  200. "error": str(e)
  201. })
  202. batch_result = {
  203. "results": results,
  204. "total_products": len(product_list),
  205. "successful": successful,
  206. "failed": failed
  207. }
  208. response_serializer = BatchProductResponseSerializer(data=batch_result)
  209. if response_serializer.is_valid():
  210. return Response(response_serializer.data, status=status.HTTP_200_OK)
  211. return Response(batch_result, status=status.HTTP_200_OK)
  212. class ProductListView(APIView):
  213. """
  214. GET API to list all products with details
  215. """
  216. def get(self, request):
  217. products = Product.objects.all()
  218. serializer = ProductSerializer(products, many=True)
  219. return Response(serializer.data, status=status.HTTP_200_OK)
  220. class ProductUploadExcelView(APIView):
  221. """
  222. POST API to upload an Excel file and add data to Product model (skip duplicates)
  223. """
  224. parser_classes = (MultiPartParser, FormParser)
  225. def post(self, request, *args, **kwargs):
  226. file_obj = request.FILES.get('file')
  227. if not file_obj:
  228. return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
  229. try:
  230. df = pd.read_excel(file_obj)
  231. df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
  232. expected_cols = {
  233. 'item_id',
  234. 'product_name',
  235. 'product_long_description',
  236. 'product_short_description',
  237. 'product_type',
  238. 'image_path'
  239. }
  240. if not expected_cols.issubset(df.columns):
  241. return Response({
  242. 'error': 'Missing required columns',
  243. 'required_columns': list(expected_cols)
  244. }, status=status.HTTP_400_BAD_REQUEST)
  245. created_count = 0
  246. skipped_count = 0
  247. for _, row in df.iterrows():
  248. item_id = row.get('item_id', '')
  249. # Check if this item already exists
  250. if Product.objects.filter(item_id=item_id).exists():
  251. skipped_count += 1
  252. continue
  253. Product.objects.create(
  254. item_id=item_id,
  255. product_name=row.get('product_name', ''),
  256. product_long_description=row.get('product_long_description', ''),
  257. product_short_description=row.get('product_short_description', ''),
  258. product_type=row.get('product_type', ''),
  259. image_path=row.get('image_path', ''),
  260. )
  261. created_count += 1
  262. return Response({
  263. 'message': f'Successfully uploaded {created_count} products.',
  264. 'skipped': f'Skipped {skipped_count} duplicates.'
  265. }, status=status.HTTP_201_CREATED)
  266. except Exception as e:
  267. return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  268. class ProductAttributesUploadView(APIView):
  269. """
  270. POST API to upload an Excel file and add mandatory/additional attributes
  271. for product types with possible values.
  272. """
  273. parser_classes = (MultiPartParser, FormParser)
  274. def post(self, request):
  275. file_obj = request.FILES.get('file')
  276. if not file_obj:
  277. return Response({"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST)
  278. try:
  279. df = pd.read_excel(file_obj)
  280. required_columns = {'product_type', 'attribute_name', 'is_mandatory', 'possible_values'}
  281. if not required_columns.issubset(df.columns):
  282. return Response({
  283. "error": f"Missing required columns. Found: {list(df.columns)}"
  284. }, status=status.HTTP_400_BAD_REQUEST)
  285. for _, row in df.iterrows():
  286. product_type_name = str(row['product_type']).strip()
  287. attr_name = str(row['attribute_name']).strip()
  288. is_mandatory = str(row['is_mandatory']).strip().lower() in ['yes', 'true', '1']
  289. possible_values = str(row.get('possible_values', '')).strip()
  290. # Get or create product type
  291. product_type, _ = ProductType.objects.get_or_create(name=product_type_name)
  292. # Get or create attribute
  293. attribute, _ = ProductAttribute.objects.get_or_create(
  294. product_type=product_type,
  295. name=attr_name,
  296. defaults={'is_mandatory': is_mandatory}
  297. )
  298. attribute.is_mandatory = is_mandatory
  299. attribute.save()
  300. # Handle possible values
  301. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  302. if possible_values:
  303. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  304. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  305. return Response({"message": "Attributes uploaded successfully."}, status=status.HTTP_201_CREATED)
  306. except Exception as e:
  307. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  308. class ProductTypeAttributesView(APIView):
  309. """
  310. API to view, create, update, and delete product type attributes and their possible values.
  311. Also supports dynamic product type creation.
  312. """
  313. def get(self, request):
  314. """
  315. Retrieve all product types with their attributes and possible values.
  316. """
  317. product_types = ProductType.objects.all()
  318. serializer = ProductTypeSerializer(product_types, many=True)
  319. # Transform the serialized data into the requested format
  320. result = []
  321. for pt in serializer.data:
  322. for attr in pt['attributes']:
  323. result.append({
  324. 'product_type': pt['name'],
  325. 'attribute_name': attr['name'],
  326. 'is_mandatory': 'Yes' if attr['is_mandatory'] else 'No',
  327. 'possible_values': ', '.join([pv['value'] for pv in attr['possible_values']])
  328. })
  329. return Response(result, status=status.HTTP_200_OK)
  330. def post(self, request):
  331. """
  332. Create a new product type or attribute with possible values.
  333. Expected payload example:
  334. {
  335. "product_type": "Hardware Screws",
  336. "attribute_name": "Material",
  337. "is_mandatory": "Yes",
  338. "possible_values": "Steel, Zinc Plated, Stainless Steel"
  339. }
  340. """
  341. try:
  342. product_type_name = request.data.get('product_type')
  343. attribute_name = request.data.get('attribute_name', '')
  344. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  345. possible_values = request.data.get('possible_values', '')
  346. if not product_type_name:
  347. return Response({
  348. "error": "product_type is required"
  349. }, status=status.HTTP_400_BAD_REQUEST)
  350. with transaction.atomic():
  351. # Get or create product type
  352. product_type, created = ProductType.objects.get_or_create(name=product_type_name)
  353. if created and not attribute_name:
  354. return Response({
  355. "message": f"Product type '{product_type_name}' created successfully",
  356. "data": {"product_type": product_type_name}
  357. }, status=status.HTTP_201_CREATED)
  358. if attribute_name:
  359. # Create attribute
  360. attribute, attr_created = ProductAttribute.objects.get_or_create(
  361. product_type=product_type,
  362. name=attribute_name,
  363. defaults={'is_mandatory': is_mandatory}
  364. )
  365. if not attr_created:
  366. return Response({
  367. "error": f"Attribute '{attribute_name}' already exists for product type '{product_type_name}'"
  368. }, status=status.HTTP_400_BAD_REQUEST)
  369. # Handle possible values
  370. if possible_values:
  371. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  372. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  373. return Response({
  374. "message": "Attribute created successfully",
  375. "data": {
  376. "product_type": product_type_name,
  377. "attribute_name": attribute_name,
  378. "is_mandatory": "Yes" if is_mandatory else "No",
  379. "possible_values": possible_values
  380. }
  381. }, status=status.HTTP_201_CREATED)
  382. return Response({
  383. "message": f"Product type '{product_type_name}' already exists",
  384. "data": {"product_type": product_type_name}
  385. }, status=status.HTTP_200_OK)
  386. except Exception as e:
  387. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  388. def put(self, request):
  389. """
  390. Update an existing product type attribute and its possible values.
  391. Expected payload example:
  392. {
  393. "product_type": "Hardware Screws",
  394. "attribute_name": "Material",
  395. "is_mandatory": "Yes",
  396. "possible_values": "Steel, Zinc Plated, Stainless Steel, Brass"
  397. }
  398. """
  399. try:
  400. product_type_name = request.data.get('product_type')
  401. attribute_name = request.data.get('attribute_name')
  402. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  403. possible_values = request.data.get('possible_values', '')
  404. if not all([product_type_name, attribute_name]):
  405. return Response({
  406. "error": "product_type and attribute_name are required"
  407. }, status=status.HTTP_400_BAD_REQUEST)
  408. with transaction.atomic():
  409. try:
  410. product_type = ProductType.objects.get(name=product_type_name)
  411. attribute = ProductAttribute.objects.get(
  412. product_type=product_type,
  413. name=attribute_name
  414. )
  415. except ProductType.DoesNotExist:
  416. return Response({
  417. "error": f"Product type '{product_type_name}' not found"
  418. }, status=status.HTTP_404_NOT_FOUND)
  419. except ProductAttribute.DoesNotExist:
  420. return Response({
  421. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  422. }, status=status.HTTP_404_NOT_FOUND)
  423. # Update attribute
  424. attribute.is_mandatory = is_mandatory
  425. attribute.save()
  426. # Update possible values
  427. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  428. if possible_values:
  429. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  430. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  431. return Response({
  432. "message": "Attribute updated successfully",
  433. "data": {
  434. "product_type": product_type_name,
  435. "attribute_name": attribute_name,
  436. "is_mandatory": "Yes" if is_mandatory else "No",
  437. "possible_values": possible_values
  438. }
  439. }, status=status.HTTP_200_OK)
  440. except Exception as e:
  441. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  442. def delete(self, request):
  443. """
  444. Delete a product type or a specific attribute.
  445. Expected payload example:
  446. {
  447. "product_type": "Hardware Screws",
  448. "attribute_name": "Material"
  449. }
  450. """
  451. try:
  452. product_type_name = request.data.get('product_type')
  453. attribute_name = request.data.get('attribute_name', '')
  454. if not product_type_name:
  455. return Response({
  456. "error": "product_type is required"
  457. }, status=status.HTTP_400_BAD_REQUEST)
  458. with transaction.atomic():
  459. try:
  460. product_type = ProductType.objects.get(name=product_type_name)
  461. except ProductType.DoesNotExist:
  462. return Response({
  463. "error": f"Product type '{product_type_name}' not found"
  464. }, status=status.HTTP_404_NOT_FOUND)
  465. if attribute_name:
  466. # Delete specific attribute
  467. try:
  468. attribute = ProductAttribute.objects.get(
  469. product_type=product_type,
  470. name=attribute_name
  471. )
  472. attribute.delete()
  473. return Response({
  474. "message": f"Attribute '{attribute_name}' deleted successfully from product type '{product_type_name}'"
  475. }, status=status.HTTP_200_OK)
  476. except ProductAttribute.DoesNotExist:
  477. return Response({
  478. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  479. }, status=status.HTTP_404_NOT_FOUND)
  480. else:
  481. # Delete entire product type
  482. product_type.delete()
  483. return Response({
  484. "message": f"Product type '{product_type_name}' and all its attributes deleted successfully"
  485. }, status=status.HTTP_200_OK)
  486. except Exception as e:
  487. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  488. class ProductTypeListView(APIView):
  489. """
  490. GET API to list all product types (only names).
  491. """
  492. def get(self, request):
  493. product_types = ProductType.objects.values_list('name', flat=True)
  494. return Response({"product_types": list(product_types)}, status=status.HTTP_200_OK)