views.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914
  1. # # #==================== views.py ====================
  2. # # from rest_framework.views import APIView
  3. # # from rest_framework.response import Response
  4. # # from rest_framework import status
  5. # # from .serializers import (
  6. # # ProductAttributeRequestSerializer,
  7. # # ProductAttributeResponseSerializer
  8. # # )
  9. # # from .services import ProductAttributeService
  10. # # class ExtractProductAttributesView(APIView):
  11. # # """
  12. # # API endpoint to extract product attributes using Groq LLM.
  13. # # POST /api/extract-attributes/
  14. # # Request Body:
  15. # # {
  16. # # "title": "Product title (optional)",
  17. # # "short_desc": "Short description (optional)",
  18. # # "long_desc": "Long description (optional)",
  19. # # "mandatory_attrs": {
  20. # # "Attribute1": ["value1", "value2", "value3"],
  21. # # "Attribute2": ["valueA", "valueB"]
  22. # # },
  23. # # "model": "llama-3.1-8b-instant (optional)",
  24. # # "extract_additional": true (optional, default: true)
  25. # # }
  26. # # Response:
  27. # # {
  28. # # "mandatory": {
  29. # # "Attribute1": "value1",
  30. # # "Attribute2": "valueA"
  31. # # },
  32. # # "additional": {
  33. # # "Color": "Blue",
  34. # # "Brand": "Example"
  35. # # }
  36. # # }
  37. # # """
  38. # # def post(self, request):
  39. # # # Validate request data
  40. # # serializer = ProductAttributeRequestSerializer(data=request.data)
  41. # # if not serializer.is_valid():
  42. # # return Response(
  43. # # {"error": serializer.errors},
  44. # # status=status.HTTP_400_BAD_REQUEST
  45. # # )
  46. # # validated_data = serializer.validated_data
  47. # # # Combine product text
  48. # # product_text = ProductAttributeService.combine_product_text(
  49. # # title=validated_data.get('title'),
  50. # # short_desc=validated_data.get('short_desc'),
  51. # # long_desc=validated_data.get('long_desc')
  52. # # )
  53. # # # Extract attributes
  54. # # result = ProductAttributeService.extract_attributes(
  55. # # product_text=product_text,
  56. # # mandatory_attrs=validated_data['mandatory_attrs'],
  57. # # model=validated_data.get('model'),
  58. # # extract_additional=validated_data.get('extract_additional', True)
  59. # # )
  60. # # # Return response
  61. # # response_serializer = ProductAttributeResponseSerializer(data=result)
  62. # # if response_serializer.is_valid():
  63. # # return Response(response_serializer.data, status=status.HTTP_200_OK)
  64. # # return Response(result, status=status.HTTP_200_OK)
  65. # from rest_framework.views import APIView
  66. # from rest_framework.response import Response
  67. # from rest_framework import status
  68. # from .serializers import (
  69. # SingleProductRequestSerializer,
  70. # BatchProductRequestSerializer,
  71. # ProductAttributeResultSerializer,
  72. # BatchProductResponseSerializer
  73. # )
  74. # from .services import ProductAttributeService
  75. # class ExtractProductAttributesView(APIView):
  76. # """
  77. # API endpoint to extract product attributes for a single product.
  78. # POST /api/extract-attributes/
  79. # Request Body:
  80. # {
  81. # "title": "Product title (optional)",
  82. # "short_desc": "Short description (optional)",
  83. # "long_desc": "Long description (optional)",
  84. # "mandatory_attrs": {
  85. # "Attribute1": ["value1", "value2", "value3"],
  86. # "Attribute2": ["valueA", "valueB"]
  87. # },
  88. # "model": "llama-3.1-8b-instant (optional)",
  89. # "extract_additional": true (optional, default: true)
  90. # }
  91. # """
  92. # def post(self, request):
  93. # serializer = SingleProductRequestSerializer(data=request.data)
  94. # if not serializer.is_valid():
  95. # return Response(
  96. # {"error": serializer.errors},
  97. # status=status.HTTP_400_BAD_REQUEST
  98. # )
  99. # validated_data = serializer.validated_data
  100. # product_text = ProductAttributeService.combine_product_text(
  101. # title=validated_data.get('title'),
  102. # short_desc=validated_data.get('short_desc'),
  103. # long_desc=validated_data.get('long_desc')
  104. # )
  105. # result = ProductAttributeService.extract_attributes(
  106. # product_text=product_text,
  107. # mandatory_attrs=validated_data['mandatory_attrs'],
  108. # model=validated_data.get('model'),
  109. # extract_additional=validated_data.get('extract_additional', True)
  110. # )
  111. # response_serializer = ProductAttributeResultSerializer(data=result)
  112. # if response_serializer.is_valid():
  113. # return Response(response_serializer.data, status=status.HTTP_200_OK)
  114. # return Response(result, status=status.HTTP_200_OK)
  115. # class BatchExtractProductAttributesView(APIView):
  116. # """
  117. # API endpoint to extract product attributes for multiple products in batch.
  118. # POST /api/batch-extract-attributes/
  119. # Request Body:
  120. # {
  121. # "products": [
  122. # {
  123. # "product_id": "prod_001",
  124. # "title": "Product 1 title",
  125. # "short_desc": "Short description",
  126. # "long_desc": "Long description"
  127. # },
  128. # {
  129. # "product_id": "prod_002",
  130. # "title": "Product 2 title",
  131. # "short_desc": "Short description"
  132. # }
  133. # ],
  134. # "mandatory_attrs": {
  135. # "Attribute1": ["value1", "value2", "value3"],
  136. # "Attribute2": ["valueA", "valueB"]
  137. # },
  138. # "model": "llama-3.1-8b-instant (optional)",
  139. # "extract_additional": true (optional, default: true)
  140. # }
  141. # Response:
  142. # {
  143. # "results": [
  144. # {
  145. # "product_id": "prod_001",
  146. # "mandatory": {...},
  147. # "additional": {...}
  148. # },
  149. # {
  150. # "product_id": "prod_002",
  151. # "mandatory": {...},
  152. # "additional": {...}
  153. # }
  154. # ],
  155. # "total_products": 2,
  156. # "successful": 2,
  157. # "failed": 0
  158. # }
  159. # """
  160. # def post(self, request):
  161. # serializer = BatchProductRequestSerializer(data=request.data)
  162. # if not serializer.is_valid():
  163. # return Response(
  164. # {"error": serializer.errors},
  165. # status=status.HTTP_400_BAD_REQUEST
  166. # )
  167. # validated_data = serializer.validated_data
  168. # # Extract attributes for all products in batch
  169. # result = ProductAttributeService.extract_attributes_batch(
  170. # products=validated_data['products'],
  171. # mandatory_attrs=validated_data['mandatory_attrs'],
  172. # model=validated_data.get('model'),
  173. # extract_additional=validated_data.get('extract_additional', True)
  174. # )
  175. # response_serializer = BatchProductResponseSerializer(data=result)
  176. # if response_serializer.is_valid():
  177. # return Response(response_serializer.data, status=status.HTTP_200_OK)
  178. # return Response(result, status=status.HTTP_200_OK)
  179. # ==================== views.py ====================
  180. from rest_framework.views import APIView
  181. from rest_framework.response import Response
  182. from rest_framework import status
  183. from .serializers import (
  184. SingleProductRequestSerializer,
  185. BatchProductRequestSerializer,
  186. ProductAttributeResultSerializer,
  187. BatchProductResponseSerializer
  188. )
  189. from .services import ProductAttributeService
  190. from .ocr_service import OCRService
  191. from .models import Product
  192. class ExtractProductAttributesView(APIView):
  193. """
  194. API endpoint to extract product attributes for a single product by item_id.
  195. Fetches product details from database.
  196. """
  197. def post(self, request):
  198. serializer = SingleProductRequestSerializer(data=request.data)
  199. if not serializer.is_valid():
  200. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  201. validated_data = serializer.validated_data
  202. item_id = validated_data.get("item_id")
  203. # Fetch product from DB
  204. try:
  205. product = Product.objects.get(item_id=item_id)
  206. except Product.DoesNotExist:
  207. return Response(
  208. {"error": f"Product with item_id '{item_id}' not found."},
  209. status=status.HTTP_404_NOT_FOUND
  210. )
  211. # Extract product details
  212. title = product.product_name
  213. short_desc = product.product_short_description
  214. long_desc = product.product_long_description
  215. image_url = product.image_path
  216. # Process image for OCR if required
  217. ocr_results = None
  218. ocr_text = None
  219. if validated_data.get("process_image", True) and image_url:
  220. ocr_service = OCRService()
  221. ocr_results = ocr_service.process_image(image_url)
  222. if ocr_results and ocr_results.get("detected_text"):
  223. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  224. ocr_results, validated_data.get("model")
  225. )
  226. ocr_results["extracted_attributes"] = ocr_attrs
  227. ocr_text = "\n".join([
  228. f"{item['text']} (confidence: {item['confidence']:.2f})"
  229. for item in ocr_results["detected_text"]
  230. ])
  231. # Combine all product text
  232. product_text = ProductAttributeService.combine_product_text(
  233. title=title,
  234. short_desc=short_desc,
  235. long_desc=long_desc,
  236. ocr_text=ocr_text
  237. )
  238. # Extract attributes
  239. result = ProductAttributeService.extract_attributes(
  240. product_text=product_text,
  241. mandatory_attrs=validated_data["mandatory_attrs"],
  242. model=validated_data.get("model"),
  243. extract_additional=validated_data.get("extract_additional", True)
  244. )
  245. # Attach OCR results if available
  246. if ocr_results:
  247. result["ocr_results"] = ocr_results
  248. response_serializer = ProductAttributeResultSerializer(data=result)
  249. if response_serializer.is_valid():
  250. return Response(response_serializer.data, status=status.HTTP_200_OK)
  251. return Response(result, status=status.HTTP_200_OK)
  252. from .models import Product
  253. # class BatchExtractProductAttributesView(APIView):
  254. # """
  255. # API endpoint to extract product attributes for multiple products in batch by item_id.
  256. # Fetches all product details from database automatically.
  257. # """
  258. # def post(self, request):
  259. # serializer = BatchProductRequestSerializer(data=request.data)
  260. # if not serializer.is_valid():
  261. # return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  262. # validated_data = serializer.validated_data
  263. # item_ids = validated_data.get("item_ids", [])
  264. # model = validated_data.get("model")
  265. # extract_additional = validated_data.get("extract_additional", True)
  266. # process_image = validated_data.get("process_image", True)
  267. # mandatory_attrs = validated_data["mandatory_attrs"]
  268. # # Fetch all products in one query
  269. # products = Product.objects.filter(item_id__in=item_ids)
  270. # found_ids = set(products.values_list("item_id", flat=True))
  271. # missing_ids = [pid for pid in item_ids if pid not in found_ids]
  272. # results = []
  273. # successful = 0
  274. # failed = 0
  275. # for product in products:
  276. # try:
  277. # title = product.product_name
  278. # short_desc = product.product_short_description
  279. # long_desc = product.product_long_description
  280. # image_url = product.image_path
  281. # ocr_results = None
  282. # ocr_text = None
  283. # if process_image and image_url:
  284. # ocr_service = OCRService()
  285. # ocr_results = ocr_service.process_image(image_url)
  286. # if ocr_results and ocr_results.get("detected_text"):
  287. # ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  288. # ocr_results, model
  289. # )
  290. # ocr_results["extracted_attributes"] = ocr_attrs
  291. # ocr_text = "\n".join([
  292. # f"{item['text']} (confidence: {item['confidence']:.2f})"
  293. # for item in ocr_results["detected_text"]
  294. # ])
  295. # product_text = ProductAttributeService.combine_product_text(
  296. # title=title,
  297. # short_desc=short_desc,
  298. # long_desc=long_desc,
  299. # ocr_text=ocr_text
  300. # )
  301. # extracted = ProductAttributeService.extract_attributes(
  302. # product_text=product_text,
  303. # mandatory_attrs=mandatory_attrs,
  304. # model=model,
  305. # extract_additional=extract_additional
  306. # )
  307. # result = {
  308. # "product_id": product.item_id,
  309. # "mandatory": extracted.get("mandatory", {}),
  310. # "additional": extracted.get("additional", {}),
  311. # }
  312. # if ocr_results:
  313. # result["ocr_results"] = ocr_results
  314. # results.append(result)
  315. # successful += 1
  316. # except Exception as e:
  317. # failed += 1
  318. # results.append({
  319. # "product_id": product.item_id,
  320. # "error": str(e)
  321. # })
  322. # # Add missing item_ids as failed entries
  323. # for mid in missing_ids:
  324. # failed += 1
  325. # results.append({
  326. # "product_id": mid,
  327. # "error": "Product not found in database"
  328. # })
  329. # batch_result = {
  330. # "results": results,
  331. # "total_products": len(item_ids),
  332. # "successful": successful,
  333. # "failed": failed
  334. # }
  335. # response_serializer = BatchProductResponseSerializer(data=batch_result)
  336. # if response_serializer.is_valid():
  337. # return Response(response_serializer.data, status=status.HTTP_200_OK)
  338. # return Response(batch_result, status=status.HTTP_200_OK)
  339. class BatchExtractProductAttributesView(APIView):
  340. """
  341. API endpoint to extract product attributes for multiple products in batch.
  342. Uses item-specific mandatory_attrs.
  343. """
  344. def post(self, request):
  345. serializer = BatchProductRequestSerializer(data=request.data)
  346. if not serializer.is_valid():
  347. return Response({"error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
  348. validated_data = serializer.validated_data
  349. # Get batch-level settings
  350. product_list = validated_data.get("products", []) # New: list of {item_id, mandatory_attrs}
  351. model = validated_data.get("model")
  352. extract_additional = validated_data.get("extract_additional", True)
  353. process_image = validated_data.get("process_image", True)
  354. # Extract all item_ids to query the database efficiently
  355. item_ids = [p['item_id'] for p in product_list]
  356. # Fetch all products in one query
  357. products_queryset = Product.objects.filter(item_id__in=item_ids)
  358. # Create a dictionary for easy lookup: item_id -> Product object
  359. product_map = {product.item_id: product for product in products_queryset}
  360. found_ids = set(product_map.keys())
  361. results = []
  362. successful = 0
  363. failed = 0
  364. for product_entry in product_list:
  365. item_id = product_entry['item_id']
  366. # Get item-specific mandatory attributes
  367. mandatory_attrs = product_entry['mandatory_attrs']
  368. if item_id not in found_ids:
  369. failed += 1
  370. results.append({
  371. "product_id": item_id,
  372. "error": "Product not found in database"
  373. })
  374. continue # Skip to the next product
  375. product = product_map[item_id]
  376. try:
  377. title = product.product_name
  378. short_desc = product.product_short_description
  379. long_desc = product.product_long_description
  380. image_url = product.image_path
  381. ocr_results = None
  382. ocr_text = None
  383. # Image Processing Logic (same as before)
  384. if process_image and image_url:
  385. ocr_service = OCRService()
  386. ocr_results = ocr_service.process_image(image_url)
  387. if ocr_results and ocr_results.get("detected_text"):
  388. # Ensure the services are designed to handle 'mandatory_attrs'
  389. # for attribute extraction from OCR text
  390. ocr_attrs = ProductAttributeService.extract_attributes_from_ocr(
  391. ocr_results, model
  392. )
  393. ocr_results["extracted_attributes"] = ocr_attrs
  394. ocr_text = "\n".join([
  395. f"{item['text']} (confidence: {item['confidence']:.2f})"
  396. for item in ocr_results["detected_text"]
  397. ])
  398. product_text = ProductAttributeService.combine_product_text(
  399. title=title,
  400. short_desc=short_desc,
  401. long_desc=long_desc,
  402. ocr_text=ocr_text
  403. )
  404. # Attribute Extraction Logic - NOW USING ITEM-SPECIFIC mandatory_attrs
  405. extracted = ProductAttributeService.extract_attributes(
  406. product_text=product_text,
  407. mandatory_attrs=mandatory_attrs, # <--- Changed: now item-specific
  408. model=model,
  409. extract_additional=extract_additional
  410. )
  411. result = {
  412. "product_id": product.item_id,
  413. "mandatory": extracted.get("mandatory", {}),
  414. "additional": extracted.get("additional", {}),
  415. }
  416. if ocr_results:
  417. result["ocr_results"] = ocr_results
  418. results.append(result)
  419. successful += 1
  420. except Exception as e:
  421. failed += 1
  422. results.append({
  423. "product_id": item_id,
  424. "error": str(e)
  425. })
  426. # No need for a separate missing_ids loop since we handle it when iterating over product_list
  427. # The list comprehension `item_ids = [p['item_id'] for p in product_list]` and the check
  428. # `if item_id not in found_ids:` now correctly handle missing products from the input list.
  429. batch_result = {
  430. "results": results,
  431. "total_products": len(product_list),
  432. "successful": successful,
  433. "failed": failed
  434. }
  435. response_serializer = BatchProductResponseSerializer(data=batch_result)
  436. if response_serializer.is_valid():
  437. return Response(response_serializer.data, status=status.HTTP_200_OK)
  438. return Response(batch_result, status=status.HTTP_200_OK)
  439. from rest_framework.views import APIView
  440. from rest_framework.response import Response
  441. from rest_framework import status
  442. from .models import Product
  443. from .serializers import ProductSerializer
  444. class ProductListView(APIView):
  445. """
  446. GET API to list all products with details
  447. """
  448. def get(self, request):
  449. products = Product.objects.all()
  450. serializer = ProductSerializer(products, many=True)
  451. return Response(serializer.data, status=status.HTTP_200_OK)
  452. import pandas as pd
  453. from rest_framework.parsers import MultiPartParser, FormParser
  454. from rest_framework.views import APIView
  455. from rest_framework.response import Response
  456. from rest_framework import status
  457. from .models import Product
  458. from .serializers import ProductSerializer
  459. class ProductUploadExcelView(APIView):
  460. """
  461. POST API to upload an Excel file and add data to Product model (skip duplicates)
  462. """
  463. parser_classes = (MultiPartParser, FormParser)
  464. def post(self, request, *args, **kwargs):
  465. file_obj = request.FILES.get('file')
  466. if not file_obj:
  467. return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
  468. try:
  469. import pandas as pd
  470. df = pd.read_excel(file_obj)
  471. df.columns = [c.strip().lower().replace(' ', '_') for c in df.columns]
  472. expected_cols = {
  473. 'item_id',
  474. 'product_name',
  475. 'product_long_description',
  476. 'product_short_description',
  477. 'product_type',
  478. 'image_path'
  479. }
  480. if not expected_cols.issubset(df.columns):
  481. return Response({
  482. 'error': 'Missing required columns',
  483. 'required_columns': list(expected_cols)
  484. }, status=status.HTTP_400_BAD_REQUEST)
  485. created_count = 0
  486. skipped_count = 0
  487. for _, row in df.iterrows():
  488. item_id = row.get('item_id', '')
  489. # Check if this item already exists
  490. if Product.objects.filter(item_id=item_id).exists():
  491. skipped_count += 1
  492. continue
  493. Product.objects.create(
  494. item_id=item_id,
  495. product_name=row.get('product_name', ''),
  496. product_long_description=row.get('product_long_description', ''),
  497. product_short_description=row.get('product_short_description', ''),
  498. product_type=row.get('product_type', ''),
  499. image_path=row.get('image_path', ''),
  500. )
  501. created_count += 1
  502. return Response({
  503. 'message': f'Successfully uploaded {created_count} products.',
  504. 'skipped': f'Skipped {skipped_count} duplicates.'
  505. }, status=status.HTTP_201_CREATED)
  506. except Exception as e:
  507. return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  508. import pandas as pd
  509. from rest_framework.views import APIView
  510. from rest_framework.response import Response
  511. from rest_framework import status
  512. from rest_framework.parsers import MultiPartParser, FormParser
  513. from .models import ProductType, ProductAttribute, AttributePossibleValue
  514. class ProductAttributesUploadView(APIView):
  515. """
  516. POST API to upload an Excel file and add mandatory/additional attributes
  517. for product types with possible values.
  518. """
  519. parser_classes = (MultiPartParser, FormParser)
  520. def post(self, request):
  521. file_obj = request.FILES.get('file')
  522. if not file_obj:
  523. return Response({"error": "No file provided."}, status=status.HTTP_400_BAD_REQUEST)
  524. try:
  525. df = pd.read_excel(file_obj)
  526. required_columns = {'product_type', 'attribute_name', 'is_mandatory', 'possible_values'}
  527. if not required_columns.issubset(df.columns):
  528. return Response({
  529. "error": f"Missing required columns. Found: {list(df.columns)}"
  530. }, status=status.HTTP_400_BAD_REQUEST)
  531. for _, row in df.iterrows():
  532. product_type_name = str(row['product_type']).strip()
  533. attr_name = str(row['attribute_name']).strip()
  534. is_mandatory = str(row['is_mandatory']).strip().lower() in ['yes', 'true', '1']
  535. possible_values = str(row.get('possible_values', '')).strip()
  536. # Get or create product type
  537. product_type, _ = ProductType.objects.get_or_create(name=product_type_name)
  538. # Get or create attribute
  539. attribute, _ = ProductAttribute.objects.get_or_create(
  540. product_type=product_type,
  541. name=attr_name,
  542. defaults={'is_mandatory': is_mandatory}
  543. )
  544. attribute.is_mandatory = is_mandatory
  545. attribute.save()
  546. # Handle possible values
  547. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  548. if possible_values:
  549. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  550. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  551. return Response({"message": "Attributes uploaded successfully."}, status=status.HTTP_201_CREATED)
  552. except Exception as e:
  553. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  554. from rest_framework.views import APIView
  555. from rest_framework.response import Response
  556. from rest_framework import status
  557. from .models import ProductType, ProductAttribute, AttributePossibleValue
  558. from .serializers import ProductTypeSerializer, ProductAttributeSerializer, AttributePossibleValueSerializer
  559. from django.db import transaction
  560. class ProductTypeAttributesView(APIView):
  561. """
  562. API to view, create, update, and delete product type attributes and their possible values.
  563. Also supports dynamic product type creation.
  564. """
  565. def get(self, request):
  566. """
  567. Retrieve all product types with their attributes and possible values.
  568. """
  569. product_types = ProductType.objects.all()
  570. serializer = ProductTypeSerializer(product_types, many=True)
  571. # Transform the serialized data into the requested format
  572. result = []
  573. for pt in serializer.data:
  574. for attr in pt['attributes']:
  575. result.append({
  576. 'product_type': pt['name'],
  577. 'attribute_name': attr['name'],
  578. 'is_mandatory': 'Yes' if attr['is_mandatory'] else 'No',
  579. 'possible_values': ', '.join([pv['value'] for pv in attr['possible_values']])
  580. })
  581. return Response(result, status=status.HTTP_200_OK)
  582. def post(self, request):
  583. """
  584. Create a new product type or attribute with possible values.
  585. Expected payload example:
  586. {
  587. "product_type": "Hardware Screws",
  588. "attribute_name": "Material", // Optional if only creating product type
  589. "is_mandatory": "Yes", // Optional if only creating product type
  590. "possible_values": "Steel, Zinc Plated, Stainless Steel" // Optional
  591. }
  592. """
  593. try:
  594. product_type_name = request.data.get('product_type')
  595. attribute_name = request.data.get('attribute_name', '')
  596. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  597. possible_values = request.data.get('possible_values', '')
  598. if not product_type_name:
  599. return Response({
  600. "error": "product_type is required"
  601. }, status=status.HTTP_400_BAD_REQUEST)
  602. with transaction.atomic():
  603. # Get or create product type
  604. product_type, created = ProductType.objects.get_or_create(name=product_type_name)
  605. if created and not attribute_name:
  606. # Only product type was created
  607. return Response({
  608. "message": f"Product type '{product_type_name}' created successfully",
  609. "data": {"product_type": product_type_name}
  610. }, status=status.HTTP_201_CREATED)
  611. if attribute_name:
  612. # Create attribute
  613. attribute, attr_created = ProductAttribute.objects.get_or_create(
  614. product_type=product_type,
  615. name=attribute_name,
  616. defaults={'is_mandatory': is_mandatory}
  617. )
  618. if not attr_created:
  619. return Response({
  620. "error": f"Attribute '{attribute_name}' already exists for product type '{product_type_name}'"
  621. }, status=status.HTTP_400_BAD_REQUEST)
  622. # Handle possible values
  623. if possible_values:
  624. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  625. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  626. return Response({
  627. "message": "Attribute created successfully",
  628. "data": {
  629. "product_type": product_type_name,
  630. "attribute_name": attribute_name,
  631. "is_mandatory": "Yes" if is_mandatory else "No",
  632. "possible_values": possible_values
  633. }
  634. }, status=status.HTTP_201_CREATED)
  635. return Response({
  636. "message": f"Product type '{product_type_name}' already exists",
  637. "data": {"product_type": product_type_name}
  638. }, status=status.HTTP_200_OK)
  639. except Exception as e:
  640. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  641. def put(self, request):
  642. """
  643. Update an existing product type attribute and its possible values.
  644. Expected payload example:
  645. {
  646. "product_type": "Hardware Screws",
  647. "attribute_name": "Material",
  648. "is_mandatory": "Yes",
  649. "possible_values": "Steel, Zinc Plated, Stainless Steel, Brass"
  650. }
  651. """
  652. try:
  653. product_type_name = request.data.get('product_type')
  654. attribute_name = request.data.get('attribute_name')
  655. is_mandatory = request.data.get('is_mandatory', '').lower() in ['yes', 'true', '1']
  656. possible_values = request.data.get('possible_values', '')
  657. if not all([product_type_name, attribute_name]):
  658. return Response({
  659. "error": "product_type and attribute_name are required"
  660. }, status=status.HTTP_400_BAD_REQUEST)
  661. with transaction.atomic():
  662. try:
  663. product_type = ProductType.objects.get(name=product_type_name)
  664. attribute = ProductAttribute.objects.get(
  665. product_type=product_type,
  666. name=attribute_name
  667. )
  668. except ProductType.DoesNotExist:
  669. return Response({
  670. "error": f"Product type '{product_type_name}' not found"
  671. }, status=status.HTTP_404_NOT_FOUND)
  672. except ProductAttribute.DoesNotExist:
  673. return Response({
  674. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  675. }, status=status.HTTP_404_NOT_FOUND)
  676. # Update attribute
  677. attribute.is_mandatory = is_mandatory
  678. attribute.save()
  679. # Update possible values
  680. AttributePossibleValue.objects.filter(attribute=attribute).delete()
  681. if possible_values:
  682. for val in [v.strip() for v in possible_values.split(',') if v.strip()]:
  683. AttributePossibleValue.objects.create(attribute=attribute, value=val)
  684. return Response({
  685. "message": "Attribute updated successfully",
  686. "data": {
  687. "product_type": product_type_name,
  688. "attribute_name": attribute_name,
  689. "is_mandatory": "Yes" if is_mandatory else "No",
  690. "possible_values": possible_values
  691. }
  692. }, status=status.HTTP_200_OK)
  693. except Exception as e:
  694. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  695. def delete(self, request):
  696. """
  697. Delete a product type or a specific attribute.
  698. Expected payload example:
  699. {
  700. "product_type": "Hardware Screws",
  701. "attribute_name": "Material" // Optional: if omitted, deletes entire product type
  702. }
  703. """
  704. try:
  705. product_type_name = request.data.get('product_type')
  706. attribute_name = request.data.get('attribute_name', '')
  707. if not product_type_name:
  708. return Response({
  709. "error": "product_type is required"
  710. }, status=status.HTTP_400_BAD_REQUEST)
  711. with transaction.atomic():
  712. try:
  713. product_type = ProductType.objects.get(name=product_type_name)
  714. except ProductType.DoesNotExist:
  715. return Response({
  716. "error": f"Product type '{product_type_name}' not found"
  717. }, status=status.HTTP_404_NOT_FOUND)
  718. if attribute_name:
  719. # Delete specific attribute
  720. try:
  721. attribute = ProductAttribute.objects.get(
  722. product_type=product_type,
  723. name=attribute_name
  724. )
  725. attribute.delete()
  726. return Response({
  727. "message": f"Attribute '{attribute_name}' deleted successfully from product type '{product_type_name}'"
  728. }, status=status.HTTP_200_OK)
  729. except ProductAttribute.DoesNotExist:
  730. return Response({
  731. "error": f"Attribute '{attribute_name}' not found for product type '{product_type_name}'"
  732. }, status=status.HTTP_404_NOT_FOUND)
  733. else:
  734. # Delete entire product type (and its attributes and possible values)
  735. product_type.delete()
  736. return Response({
  737. "message": f"Product type '{product_type_name}' and all its attributes deleted successfully"
  738. }, status=status.HTTP_200_OK)
  739. except Exception as e:
  740. return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)