views.py 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. # views.py (FULLY FIXED with comprehensive logging)
  2. from django.shortcuts import render, get_object_or_404
  3. from django.http import HttpResponse, 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. # logging.basicConfig(
  13. # level=logging.INFO, # or DEBUG if you want more detail
  14. # format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
  15. # )
  16. import os
  17. from core.services.image_scorer import ImageQualityScorer
  18. from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product
  19. from core.services.attribute_scorer import AttributeQualityScorer
  20. logger = logging.getLogger(__name__)
  21. def categorize_issues_and_suggestions(issues, suggestions):
  22. """Categorize issues and suggestions by component"""
  23. categorized = {
  24. 'attributes': {'issues': [], 'suggestions': []},
  25. 'title': {'issues': [], 'suggestions': []},
  26. 'description': {'issues': [], 'suggestions': []},
  27. 'seo': {'issues': [], 'suggestions': []},
  28. 'content_rules': {'issues': [], 'suggestions': []},
  29. 'images': {'issues': [], 'suggestions': []},
  30. 'general': {'issues': [], 'suggestions': []}
  31. }
  32. for issue in issues:
  33. issue_lower = issue.lower()
  34. if issue.startswith('Title:'):
  35. categorized['title']['issues'].append(issue.replace('Title:', '').strip())
  36. elif issue.startswith('Description:'):
  37. categorized['description']['issues'].append(issue.replace('Description:', '').strip())
  38. elif issue.startswith('SEO:'):
  39. categorized['seo']['issues'].append(issue.replace('SEO:', '').strip())
  40. elif issue.startswith('Image:'):
  41. categorized['images']['issues'].append(issue.replace('Image:', '').strip())
  42. elif any(keyword in issue_lower for keyword in ['image', 'photo', 'picture', 'resolution', 'blur', 'background']):
  43. categorized['images']['issues'].append(issue)
  44. elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  45. categorized['seo']['issues'].append(issue)
  46. elif 'content rule' in issue_lower:
  47. categorized['content_rules']['issues'].append(issue)
  48. elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']):
  49. categorized['attributes']['issues'].append(issue)
  50. else:
  51. categorized['general']['issues'].append(issue)
  52. for suggestion in suggestions:
  53. suggestion_lower = suggestion.lower()
  54. if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']):
  55. categorized['title']['suggestions'].append(suggestion)
  56. elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']):
  57. categorized['description']['suggestions'].append(suggestion)
  58. elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']):
  59. categorized['seo']['suggestions'].append(suggestion)
  60. elif any(keyword in suggestion_lower for keyword in ['image', 'photo', 'resolution', 'compress', 'background', 'white background']):
  61. categorized['images']['suggestions'].append(suggestion)
  62. elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  63. categorized['seo']['suggestions'].append(suggestion)
  64. elif 'content rule' in suggestion_lower:
  65. categorized['content_rules']['suggestions'].append(suggestion)
  66. elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize']):
  67. categorized['attributes']['suggestions'].append(suggestion)
  68. else:
  69. categorized['general']['suggestions'].append(suggestion)
  70. return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']}
  71. def resolve_image_path(image_rel_path):
  72. """
  73. Resolve relative image path to full absolute path with comprehensive logging
  74. """
  75. logger.info(f"[IMAGE PATH] Starting resolution for: {image_rel_path}")
  76. if not image_rel_path:
  77. logger.warning("[IMAGE PATH] No image path provided")
  78. return None
  79. # Log settings
  80. logger.info(f"[IMAGE PATH] MEDIA_ROOT: {settings.MEDIA_ROOT}")
  81. logger.info(f"[IMAGE PATH] MEDIA_URL: {settings.MEDIA_URL}")
  82. # Remove MEDIA_URL prefix if present
  83. cleaned_path = image_rel_path.replace(settings.MEDIA_URL, "").lstrip("/\\")
  84. logger.info(f"[IMAGE PATH] Cleaned path: {cleaned_path}")
  85. # Build full path
  86. full_path = os.path.join(settings.MEDIA_ROOT, cleaned_path)
  87. logger.info(f"[IMAGE PATH] Full path before normalization: {full_path}")
  88. # Normalize path (resolve .., ., etc.)
  89. full_path = os.path.normpath(full_path)
  90. logger.info(f"[IMAGE PATH] Normalized path: {full_path}")
  91. # Security check
  92. media_root_normalized = os.path.normpath(settings.MEDIA_ROOT)
  93. logger.info(f"[IMAGE PATH] Normalized MEDIA_ROOT: {media_root_normalized}")
  94. if not full_path.startswith(media_root_normalized):
  95. logger.error(f"[IMAGE PATH] SECURITY: Path outside MEDIA_ROOT! Path: {full_path}, MEDIA_ROOT: {media_root_normalized}")
  96. return None
  97. # Check if file exists
  98. if not os.path.exists(full_path):
  99. logger.error(f"[IMAGE PATH] FILE NOT FOUND: {full_path}")
  100. # List directory contents for debugging
  101. dir_path = os.path.dirname(full_path)
  102. if os.path.exists(dir_path):
  103. files = os.listdir(dir_path)
  104. logger.info(f"[IMAGE PATH] Files in {dir_path}: {files}")
  105. else:
  106. logger.error(f"[IMAGE PATH] Directory does not exist: {dir_path}")
  107. return None
  108. logger.info(f"[IMAGE PATH] ✓ Image file found and validated: {full_path}")
  109. return full_path
  110. @method_decorator(csrf_exempt, name='dispatch')
  111. class AttributeScoreView(View):
  112. """Enhanced API view with Image scoring support"""
  113. def __init__(self, *args, **kwargs):
  114. super().__init__(*args, **kwargs)
  115. self.scorer = AttributeQualityScorer(use_ai=True)
  116. self.image_scorer = ImageQualityScorer(use_ai=True)
  117. logger.info("[INIT] AttributeScoreView initialized with image scorer")
  118. def post(self, request, *args, **kwargs):
  119. """Score a single product with image scoring"""
  120. logger.info("[POST] ========== NEW SCORING REQUEST ==========")
  121. try:
  122. data = json.loads(request.body)
  123. logger.info(f"[POST] Request data keys: {data.keys()}")
  124. product_data = data.get('product', {})
  125. logger.info(f"[POST] Product data keys: {product_data.keys()}")
  126. sku = product_data.get('sku')
  127. use_ai = data.get('use_ai', True)
  128. if not sku:
  129. logger.error("[POST] SKU is missing")
  130. return JsonResponse({'error': 'SKU is required'}, status=400)
  131. logger.info(f"[POST] Processing SKU: {sku}")
  132. category = product_data.get('category', '')
  133. if not category:
  134. logger.error(f"[POST] Category missing for SKU: {sku}")
  135. return JsonResponse({'error': 'Category is required'}, status=400)
  136. logger.info(f"[POST] Category: {category}")
  137. # Get or create product
  138. product, created = Product.objects.get_or_create(
  139. sku=sku,
  140. defaults={
  141. 'title': product_data.get('title', ''),
  142. 'description': product_data.get('description', ''),
  143. 'short_description': product_data.get('short_description', ''),
  144. 'seo_title': product_data.get('seo_title', ''),
  145. 'seo_description': product_data.get('seo_description', ''),
  146. 'category': category,
  147. 'attributes': product_data.get('attributes', {})
  148. }
  149. )
  150. logger.info(f"[POST] Product {'created' if created else 'retrieved'}: {sku}")
  151. if not created:
  152. product.title = product_data.get('title', product.title)
  153. product.description = product_data.get('description', product.description)
  154. product.short_description = product_data.get('short_description', product.short_description)
  155. product.seo_title = product_data.get('seo_title', product.seo_title)
  156. product.seo_description = product_data.get('seo_description', product.seo_description)
  157. product.attributes = product_data.get('attributes', product.attributes)
  158. product.save()
  159. logger.info(f"[POST] Product updated: {sku}")
  160. # Get rules
  161. cache_key = f"category_rules_{category}"
  162. category_rules = cache.get(cache_key)
  163. if category_rules is None:
  164. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  165. cache.set(cache_key, category_rules, 3600)
  166. logger.info(f"[POST] Loaded {len(category_rules)} category rules for {category}")
  167. if not category_rules:
  168. logger.error(f"[POST] No attribute rules for category: {category}")
  169. return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400)
  170. # Get content rules
  171. content_cache_key = f"content_rules_{category}"
  172. content_rules = cache.get(content_cache_key)
  173. if content_rules is None:
  174. content_rules = list(
  175. ProductContentRule.objects.filter(
  176. Q(category__isnull=True) | Q(category=category)
  177. ).values()
  178. )
  179. cache.set(content_cache_key, content_rules, 3600)
  180. logger.info(f"[POST] Loaded {len(content_rules)} content rules for {category}")
  181. # Build product dict
  182. product_dict = {
  183. 'sku': product.sku,
  184. 'category': product.category,
  185. 'title': product.title,
  186. 'description': product.description,
  187. 'short_description': product.short_description,
  188. 'seo_title': product.seo_title,
  189. 'seo_description': product.seo_description,
  190. 'attributes': product.attributes
  191. }
  192. logger.info("[POST] Starting attribute and content scoring...")
  193. # Score product attributes and content
  194. score_result = self.scorer.score_product(
  195. product_dict,
  196. category_rules,
  197. content_rules=content_rules,
  198. generate_ai_suggestions=use_ai
  199. )
  200. logger.info(f"[POST] Attribute scoring complete. Score: {score_result.get('final_score')}")
  201. # ========== IMAGE SCORING ==========
  202. logger.info("[POST] ========== STARTING IMAGE SCORING ==========")
  203. image_score_result = None
  204. # Check for image_path in product_data
  205. image_rel_path = product_data.get('image_path')
  206. logger.info(f"[POST] Image path from payload: {image_rel_path}")
  207. if image_rel_path:
  208. image_path = resolve_image_path(image_rel_path)
  209. if image_path:
  210. try:
  211. logger.info(f"[POST] Calling image scorer for: {image_path}")
  212. image_score_result = self.image_scorer.score_image(
  213. product_dict,
  214. image_path=image_path
  215. )
  216. logger.info(f"[POST] ✓ Image scoring complete. Score: {image_score_result.get('image_score')}")
  217. logger.info(f"[POST] Image issues: {len(image_score_result.get('issues', []))}")
  218. logger.info(f"[POST] Image suggestions: {len(image_score_result.get('suggestions', []))}")
  219. except Exception as img_err:
  220. logger.error(f"[POST] ✗ Image scoring FAILED: {img_err}", exc_info=True)
  221. image_score_result = {
  222. 'image_score': None,
  223. 'breakdown': {},
  224. 'issues': [f'Image scoring error: {str(img_err)}'],
  225. 'suggestions': ['Check image file format and accessibility'],
  226. 'image_metadata': {}
  227. }
  228. else:
  229. logger.warning(f"[POST] Image path resolution failed for: {image_rel_path}")
  230. image_score_result = {
  231. 'image_score': None,
  232. 'breakdown': {},
  233. 'issues': ['Image file not found or inaccessible'],
  234. 'suggestions': [f'Verify image exists at path: {image_rel_path}'],
  235. 'image_metadata': {}
  236. }
  237. else:
  238. logger.warning("[POST] No image_path provided in payload")
  239. image_score_result = {
  240. 'image_score': None,
  241. 'breakdown': {},
  242. 'issues': ['No image provided'],
  243. 'suggestions': ['Upload a product image to improve quality score'],
  244. 'image_metadata': {}
  245. }
  246. logger.info("[POST] ========== IMAGE SCORING COMPLETE ==========")
  247. # Combine issues and suggestions
  248. all_issues = score_result['issues'] + (image_score_result.get('issues', []) if image_score_result else [])
  249. all_suggestions = score_result['suggestions'] + (image_score_result.get('suggestions', []) if image_score_result else [])
  250. logger.info(f"[POST] Total issues: {len(all_issues)} (Attributes: {len(score_result['issues'])}, Images: {len(image_score_result.get('issues', []))})")
  251. logger.info(f"[POST] Total suggestions: {len(all_suggestions)}")
  252. # Categorize feedback
  253. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  254. logger.info(f"[POST] Categorized feedback into {len(categorized)} categories")
  255. # Save score
  256. AttributeScore.objects.create(
  257. product=product,
  258. score=score_result['final_score'],
  259. max_score=score_result['max_score'],
  260. details={
  261. **score_result['breakdown'],
  262. 'image_score': image_score_result.get('image_score') if image_score_result else None,
  263. 'image_breakdown': image_score_result.get('breakdown', {}) if image_score_result else {}
  264. },
  265. issues=all_issues,
  266. suggestions=all_suggestions,
  267. ai_suggestions=score_result.get('ai_suggestions', {}),
  268. processing_time=score_result.get('processing_time', 0)
  269. )
  270. logger.info(f"[POST] Score saved to database for SKU: {sku}")
  271. # Enhanced response
  272. response_data = {
  273. 'success': True,
  274. 'product_sku': sku,
  275. 'created': created,
  276. 'score_result': {
  277. 'final_score': score_result['final_score'],
  278. 'max_score': score_result['max_score'],
  279. 'breakdown': score_result['breakdown'],
  280. 'image_score': image_score_result.get('image_score') if image_score_result else None,
  281. 'image_breakdown': image_score_result.get('breakdown', {}) if image_score_result else {},
  282. 'image_metadata': image_score_result.get('image_metadata', {}) if image_score_result else {},
  283. 'categorized_feedback': categorized,
  284. 'ai_suggestions': {
  285. 'content': score_result.get('ai_suggestions', {}),
  286. 'image': image_score_result.get('ai_improvements') if image_score_result else None
  287. },
  288. 'processing_time': score_result.get('processing_time', 0),
  289. 'issues': all_issues,
  290. 'suggestions': all_suggestions
  291. }
  292. }
  293. logger.info(f"[POST] ========== REQUEST COMPLETE: {sku} ==========")
  294. return JsonResponse(response_data)
  295. except json.JSONDecodeError as e:
  296. logger.error(f"[POST] JSON decode error: {e}", exc_info=True)
  297. return JsonResponse({'error': 'Invalid JSON'}, status=400)
  298. except Exception as e:
  299. logger.error(f"[POST] Unexpected error: {str(e)}", exc_info=True)
  300. return JsonResponse({'error': str(e)}, status=500)
  301. @method_decorator(csrf_exempt, name='dispatch')
  302. class BatchScoreView(View):
  303. """Batch scoring with image support"""
  304. def __init__(self, *args, **kwargs):
  305. super().__init__(*args, **kwargs)
  306. self.attribute_scorer = AttributeQualityScorer(use_ai=True)
  307. self.image_scorer = ImageQualityScorer(use_ai=True)
  308. logger.info("[INIT] BatchScoreView initialized")
  309. def get(self, request):
  310. """
  311. Automatically score all products from the database (no payload needed).
  312. """
  313. logger.info("[BATCH][GET] ========== AUTO BATCH SCORING STARTED ==========")
  314. try:
  315. # Fetch all products
  316. products = list(Product.objects.all().values())
  317. logger.info(f"[BATCH][GET] Found {len(products)} products in DB")
  318. if not products:
  319. return JsonResponse({'error': 'No products found in the database.'}, status=404)
  320. results = []
  321. errors = []
  322. for idx, product_data in enumerate(products, 1):
  323. sku = product_data.get('sku')
  324. category = product_data.get('category')
  325. logger.info(f"[BATCH][GET] [{idx}/{len(products)}] Processing: {sku}")
  326. if not sku or not category:
  327. errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  328. continue
  329. try:
  330. # Load rules
  331. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  332. if not category_rules:
  333. errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  334. continue
  335. content_rules = list(
  336. ProductContentRule.objects.filter(
  337. Q(category__isnull=True) | Q(category=category)
  338. ).values()
  339. )
  340. # Score product attributes/content
  341. score_result = self.attribute_scorer.score_product(
  342. product_data,
  343. category_rules,
  344. content_rules=content_rules,
  345. generate_ai_suggestions=True
  346. )
  347. # Score product image
  348. image_path = resolve_image_path(product_data.get('image_path'))
  349. image_score_result = None
  350. if image_path:
  351. try:
  352. logger.info(f"[BATCH][GET] Scoring image for {sku}")
  353. image_score_result = self.image_scorer.score_image(
  354. product_data,
  355. image_path=image_path
  356. )
  357. except Exception as img_err:
  358. logger.warning(f"[BATCH][GET] Image scoring failed for {sku}: {img_err}")
  359. image_score_result = {
  360. 'image_score': None,
  361. 'breakdown': {},
  362. 'issues': ['Image scoring failed'],
  363. 'suggestions': ['Check image file'],
  364. 'image_metadata': {}
  365. }
  366. else:
  367. image_score_result = {
  368. 'image_score': None,
  369. 'breakdown': {},
  370. 'issues': ['No image provided'],
  371. 'suggestions': ['Upload product image'],
  372. 'image_metadata': {}
  373. }
  374. # Combine issues and suggestions
  375. all_issues = score_result['issues'] + image_score_result.get('issues', [])
  376. all_suggestions = score_result['suggestions'] + image_score_result.get('suggestions', [])
  377. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  378. score_result['breakdown']['image_score'] = image_score_result.get('image_score')
  379. score_result['breakdown']['attributes'] = score_result['breakdown']['mandatory_fields']
  380. results.append({
  381. 'sku': sku,
  382. 'title': product_data['title'],
  383. 'description': product_data['description'],
  384. 'image_path': product_data['image_path'],
  385. 'created_at': product_data['created_at'],
  386. 'final_score': score_result['final_score'],
  387. 'max_score': score_result['max_score'],
  388. 'breakdown': score_result['breakdown'],
  389. 'image_score': image_score_result.get('image_score'),
  390. 'image_breakdown': image_score_result.get('breakdown', {}),
  391. 'image_metadata': image_score_result.get('image_metadata', {}),
  392. 'ai_suggestions': {
  393. 'content': score_result.get('ai_suggestions', {}),
  394. 'image': image_score_result.get('ai_improvements', {})
  395. },
  396. 'categorized_feedback': categorized,
  397. 'processing_time': score_result.get('processing_time', 0),
  398. 'issues': all_issues,
  399. 'suggestions': all_suggestions
  400. })
  401. except Exception as e:
  402. logger.error(f"[BATCH][GET] Error scoring {sku}: {str(e)}", exc_info=True)
  403. errors.append({'sku': sku, 'error': str(e)})
  404. logger.info(f"[BATCH][GET] ===== COMPLETE: {len(results)} success, {len(errors)} errors =====")
  405. return JsonResponse({
  406. 'success': True,
  407. 'processed': len(results),
  408. 'results': results,
  409. 'errors': errors
  410. })
  411. except Exception as e:
  412. logger.error(f"[BATCH][GET] Batch scoring error: {str(e)}", exc_info=True)
  413. return JsonResponse({'error': str(e)}, status=500)
  414. def post(self, request):
  415. logger.info("[BATCH] ========== NEW BATCH REQUEST ==========")
  416. try:
  417. data = json.loads(request.body)
  418. products = data.get('products', [])
  419. logger.info(f"[BATCH] Processing {len(products)} products")
  420. print(products)
  421. if not products:
  422. return JsonResponse({'error': 'No products provided'}, status=400)
  423. results = []
  424. errors = []
  425. for idx, product_data in enumerate(products[:100], 1):
  426. sku = product_data.get('sku')
  427. category = product_data.get('category')
  428. logger.info(f"[BATCH] [{idx}/{len(products)}] Processing: {sku}")
  429. if not sku or not category:
  430. errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  431. continue
  432. try:
  433. # Get rules
  434. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  435. if not category_rules:
  436. errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  437. continue
  438. content_rules = list(
  439. ProductContentRule.objects.filter(
  440. Q(category__isnull=True) | Q(category=category)
  441. ).values()
  442. )
  443. # Score attributes
  444. score_result = self.attribute_scorer.score_product(
  445. product_data,
  446. category_rules,
  447. content_rules=content_rules,
  448. generate_ai_suggestions=True
  449. )
  450. # Score image
  451. image_path = resolve_image_path(product_data.get('image_path'))
  452. image_score_result = None
  453. if image_path:
  454. try:
  455. logger.info(f"[BATCH] Scoring image for {sku}")
  456. image_score_result = self.image_scorer.score_image(
  457. product_data,
  458. image_path=image_path
  459. )
  460. logger.info(f"[BATCH] Image score for {sku}: {image_score_result.get('image_score')}")
  461. except Exception as img_err:
  462. logger.warning(f"[BATCH] Image scoring failed for {sku}: {img_err}")
  463. image_score_result = {
  464. 'image_score': None,
  465. 'breakdown': {},
  466. 'issues': ['Image scoring failed'],
  467. 'suggestions': ['Check image file'],
  468. 'image_metadata': {}
  469. }
  470. else:
  471. logger.info(f"[BATCH] No valid image for {sku}")
  472. image_score_result = {
  473. 'image_score': None,
  474. 'breakdown': {},
  475. 'issues': ['No image provided'],
  476. 'suggestions': ['Upload product image'],
  477. 'image_metadata': {}
  478. }
  479. all_issues = score_result['issues'] + image_score_result.get('issues', [])
  480. all_suggestions = score_result['suggestions'] + image_score_result.get('suggestions', [])
  481. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  482. results.append({
  483. 'sku': sku,
  484. 'title': product_data['title'],
  485. 'description': product_data['description'],
  486. 'image_path': image_path,
  487. 'final_score': score_result['final_score'],
  488. 'max_score': score_result['max_score'],
  489. 'breakdown': score_result['breakdown'],
  490. 'image_score': image_score_result.get('image_score'),
  491. 'image_breakdown': image_score_result.get('breakdown', {}),
  492. 'image_metadata': image_score_result.get('image_metadata', {}),
  493. 'ai_suggestions': {
  494. 'content': score_result.get('ai_suggestions', {}),
  495. 'image': image_score_result.get('ai_improvements', {})
  496. },
  497. 'categorized_feedback': categorized,
  498. 'processing_time': score_result.get('processing_time', 0),
  499. 'issues': all_issues,
  500. 'suggestions': all_suggestions
  501. })
  502. except Exception as e:
  503. logger.error(f"[BATCH] Error scoring {sku}: {str(e)}", exc_info=True)
  504. errors.append({'sku': sku, 'error': str(e)})
  505. logger.info(f"[BATCH] ========== BATCH COMPLETE: {len(results)} success, {len(errors)} errors ==========")
  506. return JsonResponse({
  507. 'success': True,
  508. 'processed': len(results),
  509. 'results': results,
  510. 'errors': errors
  511. })
  512. except Exception as e:
  513. logger.error(f"[BATCH] Batch scoring error: {str(e)}", exc_info=True)
  514. return JsonResponse({'error': str(e)}, status=500)
  515. import json
  516. import logging
  517. import concurrent.futures
  518. from django.http import JsonResponse
  519. from django.utils.decorators import method_decorator
  520. from django.views import View
  521. from django.views.decorators.csrf import csrf_exempt
  522. from django.db.models import Q
  523. from textblob import download_corpora
  524. from concurrent.futures import ThreadPoolExecutor, as_completed
  525. import time
  526. from core.models import Product, CategoryAttributeRule, ProductContentRule
  527. # from .scorers.attribute_scorer import AttributeQualityScorer
  528. # from .scorers.image_scorer import ImageQualityScorer
  529. # from .utils import resolve_image_path, categorize_issues_and_suggestions
  530. logger = logging.getLogger(__name__)
  531. # Try to ensure corpora are downloaded once at import
  532. try:
  533. download_corpora.download_all()
  534. logger.info("[INIT] TextBlob corpora verified.")
  535. except Exception as e:
  536. logger.warning(f"[INIT] Failed to auto-download TextBlob corpora: {e}")
  537. @method_decorator(csrf_exempt, name='dispatch')
  538. class BatchScoreViewV2(View):
  539. """Optimized Batch scoring with image support and parallel processing"""
  540. def __init__(self, *args, **kwargs):
  541. super().__init__(*args, **kwargs)
  542. self.attribute_scorer = AttributeQualityScorer(use_ai=True)
  543. self.image_scorer = ImageQualityScorer(use_ai=True)
  544. logger.info("[INIT] BatchScoreView initialized with AI scorers preloaded")
  545. def _process_single_product(self, product_data, idx, total):
  546. """Process a single product safely (run inside thread pool)"""
  547. start = time.time()
  548. sku = product_data.get('sku')
  549. category = product_data.get('category')
  550. img_raw_path = product_data.get('image_path')
  551. if not sku or not category:
  552. return {'sku': sku, 'error': 'Missing SKU or category'}
  553. try:
  554. logger.info(f"[BATCH][{idx}/{total}] Processing SKU: {sku}")
  555. # Fetch rules
  556. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  557. if not category_rules:
  558. return {'sku': sku, 'error': f'No attribute rules for category {category}'}
  559. content_rules = list(
  560. ProductContentRule.objects.filter(
  561. Q(category__isnull=True) | Q(category=category)
  562. ).values()
  563. )
  564. # Score attributes
  565. score_result = self.attribute_scorer.score_product(
  566. product_data,
  567. category_rules,
  568. content_rules=content_rules,
  569. generate_ai_suggestions=True
  570. )
  571. # Score image
  572. image_path = resolve_image_path(product_data.get('image_path'))
  573. image_score_result = None
  574. if image_path:
  575. try:
  576. image_score_result = self.image_scorer.score_image(
  577. product_data,
  578. image_path=image_path
  579. )
  580. except Exception as img_err:
  581. logger.warning(f"[IMG] Scoring failed for {sku}: {img_err}")
  582. image_score_result = {
  583. 'image_score': None,
  584. 'breakdown': {},
  585. 'issues': ['Image scoring failed'],
  586. 'suggestions': ['Check image file'],
  587. 'image_metadata': {}
  588. }
  589. else:
  590. image_score_result = {
  591. 'image_score': None,
  592. 'breakdown': {},
  593. 'issues': ['No image provided'],
  594. 'suggestions': ['Upload product image'],
  595. 'image_metadata': {}
  596. }
  597. # Combine results
  598. all_issues = score_result['issues'] + image_score_result.get('issues', [])
  599. all_suggestions = score_result['suggestions'] + image_score_result.get('suggestions', [])
  600. categorized = categorize_issues_and_suggestions(all_issues, all_suggestions)
  601. total_time = round(time.time() - start, 2)
  602. score_result['breakdown']['attributes'] = score_result['breakdown']['mandatory_fields']
  603. return {
  604. 'sku': sku,
  605. 'title': product_data.get('title'),
  606. 'description': product_data.get('description'),
  607. 'image_path': img_raw_path,
  608. 'final_score': score_result['final_score'],
  609. 'max_score': score_result['max_score'],
  610. 'breakdown': {**score_result['breakdown'], 'image_score': image_score_result.get('image_score')},
  611. 'image_score': image_score_result.get('image_score'),
  612. 'image_breakdown': image_score_result.get('breakdown', {}),
  613. 'image_metadata': image_score_result.get('image_metadata', {}),
  614. 'ai_suggestions': {
  615. 'content': score_result.get('ai_suggestions', {}),
  616. 'image': image_score_result.get('ai_improvements', {})
  617. },
  618. 'categorized_feedback': categorized,
  619. 'issues': all_issues,
  620. 'suggestions': all_suggestions,
  621. 'processing_time': total_time
  622. }
  623. except Exception as e:
  624. logger.error(f"[BATCH][{sku}] Error: {e}", exc_info=True)
  625. return {'sku': sku, 'error': str(e)}
  626. def _run_batch(self, products):
  627. """Run products in parallel using thread pool"""
  628. results, errors = [], []
  629. total = len(products)
  630. start_time = time.time()
  631. # Use up to 8 threads (tune based on your CPU)
  632. with ThreadPoolExecutor(max_workers=8) as executor:
  633. future_to_product = {
  634. executor.submit(self._process_single_product, product, idx + 1, total): product
  635. for idx, product in enumerate(products)
  636. }
  637. for future in as_completed(future_to_product):
  638. res = future.result()
  639. if "error" in res:
  640. errors.append(res)
  641. else:
  642. results.append(res)
  643. elapsed = round(time.time() - start_time, 2)
  644. logger.info(f"[BATCH] Completed {len(results)} products in {elapsed}s ({len(errors)} errors)")
  645. return results, errors, elapsed
  646. def get(self, request):
  647. """Score all products from DB automatically"""
  648. try:
  649. products = list(Product.objects.all().values())
  650. if not products:
  651. return JsonResponse({'error': 'No products found in database'}, status=404)
  652. results, errors, elapsed = self._run_batch(products)
  653. return JsonResponse({
  654. 'success': True,
  655. 'processed': len(results),
  656. 'results': results,
  657. 'errors': errors,
  658. 'elapsed_seconds': elapsed
  659. })
  660. except Exception as e:
  661. logger.error(f"[BATCH][GET] Error: {e}", exc_info=True)
  662. return JsonResponse({'error': str(e)}, status=500)
  663. def post(self, request):
  664. """Batch score via payload"""
  665. try:
  666. data = json.loads(request.body)
  667. products = data.get('products', [])
  668. if not products:
  669. return JsonResponse({'error': 'No products provided'}, status=400)
  670. results, errors, elapsed = self._run_batch(products)
  671. return JsonResponse({
  672. 'success': True,
  673. 'processed': len(results),
  674. 'results': results,
  675. 'errors': errors,
  676. 'elapsed_seconds': elapsed
  677. })
  678. except Exception as e:
  679. logger.error(f"[BATCH][POST] Error: {e}", exc_info=True)
  680. return JsonResponse({'error': str(e)}, status=500)
  681. @method_decorator(csrf_exempt, name='dispatch')
  682. class ContentRulesView(View):
  683. """API to manage ProductContentRules"""
  684. def get(self, request):
  685. try:
  686. category = request.GET.get('category')
  687. if category:
  688. rules = ProductContentRule.objects.filter(
  689. Q(category__isnull=True) | Q(category=category)
  690. )
  691. else:
  692. rules = ProductContentRule.objects.all()
  693. rules_data = list(rules.values())
  694. return JsonResponse({
  695. 'success': True,
  696. 'count': len(rules_data),
  697. 'rules': rules_data
  698. })
  699. except Exception as e:
  700. logger.error(f"Error fetching content rules: {e}", exc_info=True)
  701. return JsonResponse({'error': str(e)}, status=500)
  702. def post(self, request):
  703. try:
  704. data = json.loads(request.body)
  705. if 'field_name' not in data:
  706. return JsonResponse({'error': 'field_name is required'}, status=400)
  707. rule = ProductContentRule.objects.create(
  708. category=data.get('category'),
  709. field_name=data['field_name'],
  710. is_mandatory=data.get('is_mandatory', True),
  711. min_length=data.get('min_length'),
  712. max_length=data.get('max_length'),
  713. min_word_count=data.get('min_word_count'),
  714. max_word_count=data.get('max_word_count'),
  715. must_contain_keywords=data.get('must_contain_keywords', []),
  716. validation_regex=data.get('validation_regex', ''),
  717. description=data.get('description', '')
  718. )
  719. if data.get('category'):
  720. cache.delete(f"content_rules_{data['category']}")
  721. return JsonResponse({
  722. 'success': True,
  723. 'rule_id': rule.id,
  724. 'message': 'Content rule created successfully'
  725. })
  726. except Exception as e:
  727. logger.error(f"Error creating content rule: {e}", exc_info=True)
  728. return JsonResponse({'error': str(e)}, status=500)
  729. # views.py
  730. from rest_framework.views import APIView
  731. from rest_framework.response import Response
  732. from rest_framework import status
  733. import tempfile
  734. import os # Import os to remove the temporary file
  735. # from core.models import Product, CategoryAttributeRule, ProductContentRule # Not strictly needed here
  736. from .management.commands.load_excel_data import Command as ExcelLoader
  737. class ExcelUploadView(APIView):
  738. def post(self, request):
  739. excel_file = request.FILES.get('file')
  740. if not excel_file:
  741. return Response({'error': 'No file uploaded'}, status=status.HTTP_400_BAD_REQUEST)
  742. tmp_path = None
  743. try:
  744. # 1. Save uploaded file to a temporary location
  745. with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
  746. for chunk in excel_file.chunks():
  747. tmp.write(chunk)
  748. tmp_path = tmp.name
  749. # 2. Execute the modified loader command
  750. loader = ExcelLoader()
  751. # The handle method now returns a dictionary with 'success' and 'message'
  752. # We pass the path as a keyword argument to match the command structure
  753. result = loader.handle(excel_path=tmp_path)
  754. # 3. Return the response based on the loader result
  755. if result.get('success'):
  756. # Send the detailed message back in the API response
  757. return Response({'success': True, 'message': result['message']}, status=status.HTTP_200_OK)
  758. else:
  759. # Handle error case returned by the loader (e.g., file read failure)
  760. return Response({'success': False, 'error': result.get('error', 'An unknown error occurred during loading.')}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  761. except Exception as e:
  762. # Catch unexpected exceptions during the process
  763. return Response({'success': False, 'error': f'Upload failed: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  764. finally:
  765. import time
  766. import gc
  767. if tmp_path and os.path.exists(tmp_path):
  768. try:
  769. # Force garbage collection to ensure pandas releases the file handle
  770. gc.collect()
  771. time.sleep(0.3) # brief pause for Windows file handle release
  772. os.remove(tmp_path)
  773. except PermissionError:
  774. # Retry once after short delay (Windows-specific fix)
  775. time.sleep(0.5)
  776. try:
  777. os.remove(tmp_path)
  778. except Exception:
  779. pass # give up silently; OS will clean temp files later
  780. import os
  781. import urllib.parse
  782. from django.http import HttpResponse
  783. from django.conf import settings
  784. def open_outlook_mail(request):
  785. try:
  786. body_path = os.path.join(settings.BASE_DIR, 'core', 'scripts', 'mailbody.txt')
  787. with open(body_path, 'r', encoding='utf-8') as f:
  788. body = f.read().strip()
  789. subject = "ETS Competitor Intelligence - October 28, 2025"
  790. recipient = "manager@example.com"
  791. encoded_subject = urllib.parse.quote(subject)
  792. encoded_body = urllib.parse.quote(body)
  793. mailto_url = f"mailto:{recipient}?subject={encoded_subject}&body={encoded_body}"
  794. html = f"""
  795. <html>
  796. <head><title>Opening Outlook...</title></head>
  797. <body style="font-family: Arial, sans-serif; text-align: center; margin-top: 80px;">
  798. <script>
  799. window.onload = function() {{
  800. window.location.href = "{mailto_url}";
  801. }};
  802. </script>
  803. <h2>📧 Opening Outlook...</h2>
  804. <p>If Outlook does not open automatically, <a href="{mailto_url}">click here</a>.</p>
  805. </body>
  806. </html>
  807. """
  808. return HttpResponse(html)
  809. except Exception as e:
  810. return HttpResponse(f"<h3>Error:</h3><p>{str(e)}</p>", status=500)
  811. # views.py
  812. from rest_framework.views import APIView
  813. from rest_framework.response import Response
  814. from rest_framework import status
  815. from django.db.models import Avg, Count
  816. from .models import Product, AttributeScore # adjust the import path as needed
  817. from rest_framework.views import APIView
  818. from rest_framework.response import Response
  819. from rest_framework import status
  820. from django.db.models import Avg, Count
  821. class ProductTypeQualityMetricsView(APIView):
  822. """
  823. API endpoint to fetch product type quality metrics.
  824. Returns metrics for all products, ignoring query parameters.
  825. """
  826. def get(self, request):
  827. print("****************** get called")
  828. # Fetch all products, no filtering
  829. queryset = Product.objects.all()
  830. print("queryset", queryset)
  831. scored = (
  832. AttributeScore.objects.filter(product__in=queryset)
  833. .annotate(
  834. title_quality=Avg('details__title_quality'),
  835. description_quality=Avg('details__description_quality'),
  836. image_quality=Avg('details__image_score'),
  837. attributes_quality=Avg('details__attributes'),
  838. )
  839. .values('product__product_type')
  840. .annotate(
  841. product_count=Count('product', distinct=True),
  842. scored_product_count=Count('id'),
  843. avg_overall_score=Avg('score'),
  844. avg_title_quality=Avg('details__title_quality'),
  845. avg_description_quality=Avg('details__description_quality'),
  846. avg_image_quality=Avg('details__image_score'),
  847. avg_attributes_quality=Avg('details__attributes'),
  848. )
  849. )
  850. results = [
  851. {
  852. "product_type": item['product__product_type'],
  853. "product_count": item['product_count'],
  854. "scored_product_count": item['scored_product_count'],
  855. "avg_overall_score": round(item['avg_overall_score'] or 0, 2),
  856. "avg_title_quality_percent": round(item['avg_title_quality'] or 0, 2),
  857. "avg_description_quality_percent": round(item['avg_description_quality'] or 0, 2),
  858. "avg_image_quality_percent": round(item['avg_image_quality'] or 0, 2),
  859. "avg_attributes_quality_percent": round(item['avg_attributes_quality'] or 0, 2),
  860. }
  861. for item in scored
  862. ]
  863. return Response(results, status=status.HTTP_200_OK)
  864. # class ProductTypeQualityMetricsView(APIView):
  865. """
  866. API endpoint to fetch product type quality metrics.
  867. Supports optional ?product_type=<type> query param.
  868. """
  869. def get(self, request):
  870. product_type_filter = request.query_params.get('product_type', None)
  871. queryset = Product.objects.all()
  872. print("queryset",queryset)
  873. if product_type_filter:
  874. queryset = queryset.filter(product_type=product_type_filter)
  875. scored = (
  876. AttributeScore.objects.filter(product__in=queryset)
  877. .annotate(
  878. title_quality=Avg('details__title_quality'),
  879. description_quality=Avg('details__description_quality'),
  880. image_quality=Avg('details__image_score'),
  881. attributes_quality=Avg('details__attributes'),
  882. )
  883. .values('product__category')
  884. .annotate(
  885. product_count=Count('product', distinct=True),
  886. scored_product_count=Count('id'),
  887. avg_overall_score=Avg('score'),
  888. avg_title_quality=Avg('details__title_quality'),
  889. avg_description_quality=Avg('details__description_quality'),
  890. avg_image_quality=Avg('details__image_score'),
  891. avg_attributes_quality=Avg('details__attributes'),
  892. )
  893. )
  894. results = [
  895. {
  896. "product_type": item['product__product_type'],
  897. "product_count": item['product_count'],
  898. "scored_product_count": item['scored_product_count'],
  899. "avg_overall_score": round(item['avg_overall_score'] or 0, 2),
  900. "avg_title_quality_percent": round(item['avg_title_quality'] or 0, 2),
  901. "avg_description_quality_percent": round(item['avg_description_quality'] or 0, 2),
  902. "avg_image_quality_percent": round(item['avg_image_quality'] or 0, 2),
  903. "avg_attributes_quality_percent": round(item['avg_attributes_quality'] or 0, 2),
  904. }
  905. for item in scored
  906. ]
  907. return Response(results, status=status.HTTP_200_OK)