views.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. # views.py (FULLY FIXED with comprehensive logging)
  2. from django.shortcuts import render, get_object_or_404
  3. from django.http import JsonResponse
  4. from django.views import View
  5. from django.core.cache import cache
  6. from django.db.models import Q
  7. from django.views.decorators.csrf import csrf_exempt
  8. from django.utils.decorators import method_decorator
  9. from django.conf import settings
  10. import json
  11. import logging
  12. import os
  13. from core.services.image_scorer import ImageQualityScorer
  14. from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product
  15. from core.services.attribute_scorer import AttributeQualityScorer
  16. logger = logging.getLogger(__name__)
  17. def categorize_issues_and_suggestions(issues, suggestions):
  18. """Categorize issues and suggestions by component"""
  19. categorized = {
  20. 'attributes': {'issues': [], 'suggestions': []},
  21. 'title': {'issues': [], 'suggestions': []},
  22. 'description': {'issues': [], 'suggestions': []},
  23. 'seo': {'issues': [], 'suggestions': []},
  24. 'content_rules': {'issues': [], 'suggestions': []},
  25. 'images': {'issues': [], 'suggestions': []},
  26. 'general': {'issues': [], 'suggestions': []}
  27. }
  28. for issue in issues:
  29. issue_lower = issue.lower()
  30. if issue.startswith('Title:'):
  31. categorized['title']['issues'].append(issue.replace('Title:', '').strip())
  32. elif issue.startswith('Description:'):
  33. categorized['description']['issues'].append(issue.replace('Description:', '').strip())
  34. elif issue.startswith('SEO:'):
  35. categorized['seo']['issues'].append(issue.replace('SEO:', '').strip())
  36. elif issue.startswith('Image:'):
  37. categorized['images']['issues'].append(issue.replace('Image:', '').strip())
  38. elif any(keyword in issue_lower for keyword in ['image', 'photo', 'picture', 'resolution', 'blur', 'background']):
  39. categorized['images']['issues'].append(issue)
  40. elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  41. categorized['seo']['issues'].append(issue)
  42. elif 'content rule' in issue_lower:
  43. categorized['content_rules']['issues'].append(issue)
  44. elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']):
  45. categorized['attributes']['issues'].append(issue)
  46. else:
  47. categorized['general']['issues'].append(issue)
  48. for suggestion in suggestions:
  49. suggestion_lower = suggestion.lower()
  50. if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']):
  51. categorized['title']['suggestions'].append(suggestion)
  52. elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']):
  53. categorized['description']['suggestions'].append(suggestion)
  54. elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']):
  55. categorized['seo']['suggestions'].append(suggestion)
  56. elif any(keyword in suggestion_lower for keyword in ['image', 'photo', 'resolution', 'compress', 'background', 'white background']):
  57. categorized['images']['suggestions'].append(suggestion)
  58. elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  59. categorized['seo']['suggestions'].append(suggestion)
  60. elif 'content rule' in suggestion_lower:
  61. categorized['content_rules']['suggestions'].append(suggestion)
  62. elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize']):
  63. categorized['attributes']['suggestions'].append(suggestion)
  64. else:
  65. categorized['general']['suggestions'].append(suggestion)
  66. return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']}
  67. def resolve_image_path(image_rel_path):
  68. """
  69. Resolve relative image path to full absolute path with comprehensive logging
  70. """
  71. logger.info(f"[IMAGE PATH] Starting resolution for: {image_rel_path}")
  72. if not image_rel_path:
  73. logger.warning("[IMAGE PATH] No image path provided")
  74. return None
  75. # Log settings
  76. logger.info(f"[IMAGE PATH] MEDIA_ROOT: {settings.MEDIA_ROOT}")
  77. logger.info(f"[IMAGE PATH] MEDIA_URL: {settings.MEDIA_URL}")
  78. # Remove MEDIA_URL prefix if present
  79. cleaned_path = image_rel_path.replace(settings.MEDIA_URL, "").lstrip("/\\")
  80. logger.info(f"[IMAGE PATH] Cleaned path: {cleaned_path}")
  81. # Build full path
  82. full_path = os.path.join(settings.MEDIA_ROOT, cleaned_path)
  83. logger.info(f"[IMAGE PATH] Full path before normalization: {full_path}")
  84. # Normalize path (resolve .., ., etc.)
  85. full_path = os.path.normpath(full_path)
  86. logger.info(f"[IMAGE PATH] Normalized path: {full_path}")
  87. # Security check
  88. media_root_normalized = os.path.normpath(settings.MEDIA_ROOT)
  89. logger.info(f"[IMAGE PATH] Normalized MEDIA_ROOT: {media_root_normalized}")
  90. if not full_path.startswith(media_root_normalized):
  91. logger.error(f"[IMAGE PATH] SECURITY: Path outside MEDIA_ROOT! Path: {full_path}, MEDIA_ROOT: {media_root_normalized}")
  92. return None
  93. # Check if file exists
  94. if not os.path.exists(full_path):
  95. logger.error(f"[IMAGE PATH] FILE NOT FOUND: {full_path}")
  96. # List directory contents for debugging
  97. dir_path = os.path.dirname(full_path)
  98. if os.path.exists(dir_path):
  99. files = os.listdir(dir_path)
  100. logger.info(f"[IMAGE PATH] Files in {dir_path}: {files}")
  101. else:
  102. logger.error(f"[IMAGE PATH] Directory does not exist: {dir_path}")
  103. return None
  104. logger.info(f"[IMAGE PATH] ✓ Image file found and validated: {full_path}")
  105. return full_path
  106. @method_decorator(csrf_exempt, name='dispatch')
  107. class AttributeScoreView(View):
  108. """Enhanced API view with Image scoring support"""
  109. def __init__(self, *args, **kwargs):
  110. super().__init__(*args, **kwargs)
  111. self.scorer = AttributeQualityScorer(use_ai=True)
  112. self.image_scorer = ImageQualityScorer(use_ai=True)
  113. logger.info("[INIT] AttributeScoreView initialized with image scorer")
  114. def post(self, request, *args, **kwargs):
  115. """Score a single product with image scoring"""
  116. logger.info("[POST] ========== NEW SCORING REQUEST ==========")
  117. try:
  118. data = json.loads(request.body)
  119. logger.info(f"[POST] Request data keys: {data.keys()}")
  120. product_data = data.get('product', {})
  121. logger.info(f"[POST] Product data keys: {product_data.keys()}")
  122. sku = product_data.get('sku')
  123. use_ai = data.get('use_ai', True)
  124. if not sku:
  125. logger.error("[POST] SKU is missing")
  126. return JsonResponse({'error': 'SKU is required'}, status=400)
  127. logger.info(f"[POST] Processing SKU: {sku}")
  128. category = product_data.get('category', '')
  129. if not category:
  130. logger.error(f"[POST] Category missing for SKU: {sku}")
  131. return JsonResponse({'error': 'Category is required'}, status=400)
  132. logger.info(f"[POST] Category: {category}")
  133. # Get or create product
  134. product, created = Product.objects.get_or_create(
  135. sku=sku,
  136. defaults={
  137. 'title': product_data.get('title', ''),
  138. 'description': product_data.get('description', ''),
  139. 'short_description': product_data.get('short_description', ''),
  140. 'seo_title': product_data.get('seo_title', ''),
  141. 'seo_description': product_data.get('seo_description', ''),
  142. 'category': category,
  143. 'attributes': product_data.get('attributes', {})
  144. }
  145. )
  146. logger.info(f"[POST] Product {'created' if created else 'retrieved'}: {sku}")
  147. if not created:
  148. product.title = product_data.get('title', product.title)
  149. product.description = product_data.get('description', product.description)
  150. product.short_description = product_data.get('short_description', product.short_description)
  151. product.seo_title = product_data.get('seo_title', product.seo_title)
  152. product.seo_description = product_data.get('seo_description', product.seo_description)
  153. product.attributes = product_data.get('attributes', product.attributes)
  154. product.save()
  155. logger.info(f"[POST] Product updated: {sku}")
  156. # Get rules
  157. cache_key = f"category_rules_{category}"
  158. category_rules = cache.get(cache_key)
  159. if category_rules is None:
  160. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  161. cache.set(cache_key, category_rules, 3600)
  162. logger.info(f"[POST] Loaded {len(category_rules)} category rules for {category}")
  163. if not category_rules:
  164. logger.error(f"[POST] No attribute rules for category: {category}")
  165. return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400)
  166. # Get content rules
  167. content_cache_key = f"content_rules_{category}"
  168. content_rules = cache.get(content_cache_key)
  169. if content_rules is None:
  170. content_rules = list(
  171. ProductContentRule.objects.filter(
  172. Q(category__isnull=True) | Q(category=category)
  173. ).values()
  174. )
  175. cache.set(content_cache_key, content_rules, 3600)
  176. logger.info(f"[POST] Loaded {len(content_rules)} content rules for {category}")
  177. # Build product dict
  178. product_dict = {
  179. 'sku': product.sku,
  180. 'category': product.category,
  181. 'title': product.title,
  182. 'description': product.description,
  183. 'short_description': product.short_description,
  184. 'seo_title': product.seo_title,
  185. 'seo_description': product.seo_description,
  186. 'attributes': product.attributes
  187. }
  188. logger.info("[POST] Starting attribute and content scoring...")
  189. # Score product attributes and content
  190. score_result = self.scorer.score_product(
  191. product_dict,
  192. category_rules,
  193. content_rules=content_rules,
  194. generate_ai_suggestions=use_ai
  195. )
  196. logger.info(f"[POST] Attribute scoring complete. Score: {score_result.get('final_score')}")
  197. # ========== IMAGE SCORING ==========
  198. logger.info("[POST] ========== STARTING IMAGE SCORING ==========")
  199. image_score_result = None
  200. # Check for image_path in product_data
  201. image_rel_path = product_data.get('image_path')
  202. logger.info(f"[POST] Image path from payload: {image_rel_path}")
  203. if image_rel_path:
  204. image_path = resolve_image_path(image_rel_path)
  205. if image_path:
  206. try:
  207. logger.info(f"[POST] Calling image scorer for: {image_path}")
  208. image_score_result = self.image_scorer.score_image(
  209. product_dict,
  210. image_path=image_path
  211. )
  212. logger.info(f"[POST] ✓ Image scoring complete. Score: {image_score_result.get('image_score')}")
  213. logger.info(f"[POST] Image issues: {len(image_score_result.get('issues', []))}")
  214. logger.info(f"[POST] Image suggestions: {len(image_score_result.get('suggestions', []))}")
  215. except Exception as img_err:
  216. logger.error(f"[POST] ✗ Image scoring FAILED: {img_err}", exc_info=True)
  217. image_score_result = {
  218. 'image_score': None,
  219. 'breakdown': {},
  220. 'issues': [f'Image scoring error: {str(img_err)}'],
  221. 'suggestions': ['Check image file format and accessibility'],
  222. 'image_metadata': {}
  223. }
  224. else:
  225. logger.warning(f"[POST] Image path resolution failed for: {image_rel_path}")
  226. image_score_result = {
  227. 'image_score': None,
  228. 'breakdown': {},
  229. 'issues': ['Image file not found or inaccessible'],
  230. 'suggestions': [f'Verify image exists at path: {image_rel_path}'],
  231. 'image_metadata': {}
  232. }
  233. else:
  234. logger.warning("[POST] No image_path provided in payload")
  235. image_score_result = {
  236. 'image_score': None,
  237. 'breakdown': {},
  238. 'issues': ['No image provided'],
  239. 'suggestions': ['Upload a product image to improve quality score'],
  240. 'image_metadata': {}
  241. }
  242. logger.info("[POST] ========== IMAGE SCORING COMPLETE ==========")
  243. # Combine issues and suggestions
  244. all_issues = score_result['issues'] + (image_score_result.get('issues', []) if image_score_result else [])
  245. all_suggestions = score_result['suggestions'] + (image_score_result.get('suggestions', []) if image_score_result else [])
  246. logger.info(f"[POST] Total issues: {len(all_issues)} (Attributes: {len(score_result['issues'])}, Images: {len(image_score_result.get('issues', []))})")
  247. logger.info(f"[POST] Total suggestions: {len(all_suggestions)}")
  248. # Categorize feedback
  249. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  250. logger.info(f"[POST] Categorized feedback into {len(categorized)} categories")
  251. # Save score
  252. AttributeScore.objects.create(
  253. product=product,
  254. score=score_result['final_score'],
  255. max_score=score_result['max_score'],
  256. details={
  257. **score_result['breakdown'],
  258. 'image_score': image_score_result.get('image_score') if image_score_result else None,
  259. 'image_breakdown': image_score_result.get('breakdown', {}) if image_score_result else {}
  260. },
  261. issues=all_issues,
  262. suggestions=all_suggestions,
  263. ai_suggestions=score_result.get('ai_suggestions', {}),
  264. processing_time=score_result.get('processing_time', 0)
  265. )
  266. logger.info(f"[POST] Score saved to database for SKU: {sku}")
  267. # Enhanced response
  268. response_data = {
  269. 'success': True,
  270. 'product_sku': sku,
  271. 'created': created,
  272. 'score_result': {
  273. 'final_score': score_result['final_score'],
  274. 'max_score': score_result['max_score'],
  275. 'breakdown': score_result['breakdown'],
  276. 'image_score': image_score_result.get('image_score') if image_score_result else None,
  277. 'image_breakdown': image_score_result.get('breakdown', {}) if image_score_result else {},
  278. 'image_metadata': image_score_result.get('image_metadata', {}) if image_score_result else {},
  279. 'categorized_feedback': categorized,
  280. 'ai_suggestions': {
  281. 'content': score_result.get('ai_suggestions', {}),
  282. 'image': image_score_result.get('ai_improvements') if image_score_result else None
  283. },
  284. 'processing_time': score_result.get('processing_time', 0),
  285. 'issues': all_issues,
  286. 'suggestions': all_suggestions
  287. }
  288. }
  289. logger.info(f"[POST] ========== REQUEST COMPLETE: {sku} ==========")
  290. return JsonResponse(response_data)
  291. except json.JSONDecodeError as e:
  292. logger.error(f"[POST] JSON decode error: {e}", exc_info=True)
  293. return JsonResponse({'error': 'Invalid JSON'}, status=400)
  294. except Exception as e:
  295. logger.error(f"[POST] Unexpected error: {str(e)}", exc_info=True)
  296. return JsonResponse({'error': str(e)}, status=500)
  297. @method_decorator(csrf_exempt, name='dispatch')
  298. class BatchScoreView(View):
  299. """Batch scoring with image support"""
  300. def __init__(self, *args, **kwargs):
  301. super().__init__(*args, **kwargs)
  302. self.attribute_scorer = AttributeQualityScorer(use_ai=True)
  303. self.image_scorer = ImageQualityScorer(use_ai=True)
  304. logger.info("[INIT] BatchScoreView initialized")
  305. def get(self, request):
  306. """
  307. Automatically score all products from the database (no payload needed).
  308. """
  309. logger.info("[BATCH][GET] ========== AUTO BATCH SCORING STARTED ==========")
  310. try:
  311. # Fetch all products
  312. products = list(Product.objects.all().values())
  313. logger.info(f"[BATCH][GET] Found {len(products)} products in DB")
  314. if not products:
  315. return JsonResponse({'error': 'No products found in the database.'}, status=404)
  316. results = []
  317. errors = []
  318. for idx, product_data in enumerate(products, 1):
  319. sku = product_data.get('sku')
  320. category = product_data.get('category')
  321. logger.info(f"[BATCH][GET] [{idx}/{len(products)}] Processing: {sku}")
  322. if not sku or not category:
  323. errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  324. continue
  325. try:
  326. # Load rules
  327. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  328. if not category_rules:
  329. errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  330. continue
  331. content_rules = list(
  332. ProductContentRule.objects.filter(
  333. Q(category__isnull=True) | Q(category=category)
  334. ).values()
  335. )
  336. # Score product attributes/content
  337. score_result = self.attribute_scorer.score_product(
  338. product_data,
  339. category_rules,
  340. content_rules=content_rules,
  341. generate_ai_suggestions=True
  342. )
  343. # Score product image
  344. image_path = resolve_image_path(product_data.get('image_path'))
  345. image_score_result = None
  346. if image_path:
  347. try:
  348. logger.info(f"[BATCH][GET] Scoring image for {sku}")
  349. image_score_result = self.image_scorer.score_image(
  350. product_data,
  351. image_path=image_path
  352. )
  353. except Exception as img_err:
  354. logger.warning(f"[BATCH][GET] Image scoring failed for {sku}: {img_err}")
  355. image_score_result = {
  356. 'image_score': None,
  357. 'breakdown': {},
  358. 'issues': ['Image scoring failed'],
  359. 'suggestions': ['Check image file'],
  360. 'image_metadata': {}
  361. }
  362. else:
  363. image_score_result = {
  364. 'image_score': None,
  365. 'breakdown': {},
  366. 'issues': ['No image provided'],
  367. 'suggestions': ['Upload product image'],
  368. 'image_metadata': {}
  369. }
  370. # Combine issues and suggestions
  371. all_issues = score_result['issues'] + image_score_result.get('issues', [])
  372. all_suggestions = score_result['suggestions'] + image_score_result.get('suggestions', [])
  373. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  374. score_result['breakdown']['image_score'] = image_score_result.get('image_score')
  375. results.append({
  376. 'sku': sku,
  377. 'title': product_data['title'],
  378. 'description': product_data['description'],
  379. 'image_path': product_data['image_path'],
  380. 'created_at': product_data['created_at'],
  381. 'final_score': score_result['final_score'],
  382. 'max_score': score_result['max_score'],
  383. 'breakdown': score_result['breakdown'],
  384. 'image_score': image_score_result.get('image_score'),
  385. 'image_breakdown': image_score_result.get('breakdown', {}),
  386. 'image_metadata': image_score_result.get('image_metadata', {}),
  387. 'ai_suggestions': {
  388. 'content': score_result.get('ai_suggestions', {}),
  389. 'image': image_score_result.get('ai_improvements', {})
  390. },
  391. 'categorized_feedback': categorized,
  392. 'processing_time': score_result.get('processing_time', 0),
  393. 'issues': all_issues,
  394. 'suggestions': all_suggestions
  395. })
  396. except Exception as e:
  397. logger.error(f"[BATCH][GET] Error scoring {sku}: {str(e)}", exc_info=True)
  398. errors.append({'sku': sku, 'error': str(e)})
  399. logger.info(f"[BATCH][GET] ===== COMPLETE: {len(results)} success, {len(errors)} errors =====")
  400. return JsonResponse({
  401. 'success': True,
  402. 'processed': len(results),
  403. 'results': results,
  404. 'errors': errors
  405. })
  406. except Exception as e:
  407. logger.error(f"[BATCH][GET] Batch scoring error: {str(e)}", exc_info=True)
  408. return JsonResponse({'error': str(e)}, status=500)
  409. def post(self, request):
  410. logger.info("[BATCH] ========== NEW BATCH REQUEST ==========")
  411. try:
  412. data = json.loads(request.body)
  413. products = data.get('products', [])
  414. logger.info(f"[BATCH] Processing {len(products)} products")
  415. print(products)
  416. if not products:
  417. return JsonResponse({'error': 'No products provided'}, status=400)
  418. results = []
  419. errors = []
  420. for idx, product_data in enumerate(products[:100], 1):
  421. sku = product_data.get('sku')
  422. category = product_data.get('category')
  423. logger.info(f"[BATCH] [{idx}/{len(products)}] Processing: {sku}")
  424. if not sku or not category:
  425. errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  426. continue
  427. try:
  428. # Get rules
  429. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  430. if not category_rules:
  431. errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  432. continue
  433. content_rules = list(
  434. ProductContentRule.objects.filter(
  435. Q(category__isnull=True) | Q(category=category)
  436. ).values()
  437. )
  438. # Score attributes
  439. score_result = self.attribute_scorer.score_product(
  440. product_data,
  441. category_rules,
  442. content_rules=content_rules,
  443. generate_ai_suggestions=True
  444. )
  445. # Score image
  446. image_path = resolve_image_path(product_data.get('image_path'))
  447. image_score_result = None
  448. if image_path:
  449. try:
  450. logger.info(f"[BATCH] Scoring image for {sku}")
  451. image_score_result = self.image_scorer.score_image(
  452. product_data,
  453. image_path=image_path
  454. )
  455. logger.info(f"[BATCH] Image score for {sku}: {image_score_result.get('image_score')}")
  456. except Exception as img_err:
  457. logger.warning(f"[BATCH] Image scoring failed for {sku}: {img_err}")
  458. image_score_result = {
  459. 'image_score': None,
  460. 'breakdown': {},
  461. 'issues': ['Image scoring failed'],
  462. 'suggestions': ['Check image file'],
  463. 'image_metadata': {}
  464. }
  465. else:
  466. logger.info(f"[BATCH] No valid image for {sku}")
  467. image_score_result = {
  468. 'image_score': None,
  469. 'breakdown': {},
  470. 'issues': ['No image provided'],
  471. 'suggestions': ['Upload product image'],
  472. 'image_metadata': {}
  473. }
  474. all_issues = score_result['issues'] + image_score_result.get('issues', [])
  475. all_suggestions = score_result['suggestions'] + image_score_result.get('suggestions', [])
  476. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  477. results.append({
  478. 'sku': sku,
  479. 'title': product_data['title'],
  480. 'description': product_data['description'],
  481. 'image_path': image_path,
  482. 'final_score': score_result['final_score'],
  483. 'max_score': score_result['max_score'],
  484. 'breakdown': score_result['breakdown'],
  485. 'image_score': image_score_result.get('image_score'),
  486. 'image_breakdown': image_score_result.get('breakdown', {}),
  487. 'image_metadata': image_score_result.get('image_metadata', {}),
  488. 'ai_suggestions': {
  489. 'content': score_result.get('ai_suggestions', {}),
  490. 'image': image_score_result.get('ai_improvements', {})
  491. },
  492. 'categorized_feedback': categorized,
  493. 'processing_time': score_result.get('processing_time', 0),
  494. 'issues': all_issues,
  495. 'suggestions': all_suggestions
  496. })
  497. except Exception as e:
  498. logger.error(f"[BATCH] Error scoring {sku}: {str(e)}", exc_info=True)
  499. errors.append({'sku': sku, 'error': str(e)})
  500. logger.info(f"[BATCH] ========== BATCH COMPLETE: {len(results)} success, {len(errors)} errors ==========")
  501. return JsonResponse({
  502. 'success': True,
  503. 'processed': len(results),
  504. 'results': results,
  505. 'errors': errors
  506. })
  507. except Exception as e:
  508. logger.error(f"[BATCH] Batch scoring error: {str(e)}", exc_info=True)
  509. return JsonResponse({'error': str(e)}, status=500)
  510. @method_decorator(csrf_exempt, name='dispatch')
  511. class ContentRulesView(View):
  512. """API to manage ProductContentRules"""
  513. def get(self, request):
  514. try:
  515. category = request.GET.get('category')
  516. if category:
  517. rules = ProductContentRule.objects.filter(
  518. Q(category__isnull=True) | Q(category=category)
  519. )
  520. else:
  521. rules = ProductContentRule.objects.all()
  522. rules_data = list(rules.values())
  523. return JsonResponse({
  524. 'success': True,
  525. 'count': len(rules_data),
  526. 'rules': rules_data
  527. })
  528. except Exception as e:
  529. logger.error(f"Error fetching content rules: {e}", exc_info=True)
  530. return JsonResponse({'error': str(e)}, status=500)
  531. def post(self, request):
  532. try:
  533. data = json.loads(request.body)
  534. if 'field_name' not in data:
  535. return JsonResponse({'error': 'field_name is required'}, status=400)
  536. rule = ProductContentRule.objects.create(
  537. category=data.get('category'),
  538. field_name=data['field_name'],
  539. is_mandatory=data.get('is_mandatory', True),
  540. min_length=data.get('min_length'),
  541. max_length=data.get('max_length'),
  542. min_word_count=data.get('min_word_count'),
  543. max_word_count=data.get('max_word_count'),
  544. must_contain_keywords=data.get('must_contain_keywords', []),
  545. validation_regex=data.get('validation_regex', ''),
  546. description=data.get('description', '')
  547. )
  548. if data.get('category'):
  549. cache.delete(f"content_rules_{data['category']}")
  550. return JsonResponse({
  551. 'success': True,
  552. 'rule_id': rule.id,
  553. 'message': 'Content rule created successfully'
  554. })
  555. except Exception as e:
  556. logger.error(f"Error creating content rule: {e}", exc_info=True)
  557. return JsonResponse({'error': str(e)}, status=500)
  558. # views.py
  559. from rest_framework.views import APIView
  560. from rest_framework.response import Response
  561. from rest_framework import status
  562. import tempfile
  563. import os # Import os to remove the temporary file
  564. # from core.models import Product, CategoryAttributeRule, ProductContentRule # Not strictly needed here
  565. from .management.commands.load_excel_data import Command as ExcelLoader
  566. class ExcelUploadView(APIView):
  567. def post(self, request):
  568. excel_file = request.FILES.get('file')
  569. if not excel_file:
  570. return Response({'error': 'No file uploaded'}, status=status.HTTP_400_BAD_REQUEST)
  571. tmp_path = None
  572. try:
  573. # 1. Save uploaded file to a temporary location
  574. with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
  575. for chunk in excel_file.chunks():
  576. tmp.write(chunk)
  577. tmp_path = tmp.name
  578. # 2. Execute the modified loader command
  579. loader = ExcelLoader()
  580. # The handle method now returns a dictionary with 'success' and 'message'
  581. # We pass the path as a keyword argument to match the command structure
  582. result = loader.handle(excel_path=tmp_path)
  583. # 3. Return the response based on the loader result
  584. if result.get('success'):
  585. # Send the detailed message back in the API response
  586. return Response({'success': True, 'message': result['message']}, status=status.HTTP_200_OK)
  587. else:
  588. # Handle error case returned by the loader (e.g., file read failure)
  589. return Response({'success': False, 'error': result.get('error', 'An unknown error occurred during loading.')}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  590. except Exception as e:
  591. # Catch unexpected exceptions during the process
  592. return Response({'success': False, 'error': f'Upload failed: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  593. finally:
  594. import time
  595. import gc
  596. if tmp_path and os.path.exists(tmp_path):
  597. try:
  598. # Force garbage collection to ensure pandas releases the file handle
  599. gc.collect()
  600. time.sleep(0.3) # brief pause for Windows file handle release
  601. os.remove(tmp_path)
  602. except PermissionError:
  603. # Retry once after short delay (Windows-specific fix)
  604. time.sleep(0.5)
  605. try:
  606. os.remove(tmp_path)
  607. except Exception:
  608. pass # give up silently; OS will clean temp files later