views.py 64 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622
  1. # # views.py (Enhanced)
  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. # import json
  7. # import logging
  8. # from core.models import AttributeScore, CategoryAttributeRule, Product
  9. # from core.services.attribute_scorer import AttributeQualityScorer
  10. # from django.views.decorators.csrf import csrf_exempt
  11. # from django.utils.decorators import method_decorator
  12. # logger = logging.getLogger(__name__)
  13. # @method_decorator(csrf_exempt, name='dispatch')
  14. # class AttributeScoreView(View):
  15. # """Enhanced API view with caching and AI suggestions"""
  16. # def __init__(self, *args, **kwargs):
  17. # super().__init__(*args, **kwargs)
  18. # self.scorer = AttributeQualityScorer(use_ai=True) # enable AI
  19. # def post(self, request, *args, **kwargs):
  20. # """Score a single product with AI suggestions"""
  21. # try:
  22. # data = json.loads(request.body)
  23. # product_data = data.get('product', {})
  24. # sku = product_data.get('sku')
  25. # use_ai = data.get('use_ai', True)
  26. # if not sku:
  27. # return JsonResponse({'error': 'SKU is required'}, status=400)
  28. # category = product_data.get('category', '')
  29. # if not category:
  30. # return JsonResponse({'error': 'Category is required'}, status=400)
  31. # # Get or create product
  32. # product, created = Product.objects.get_or_create(
  33. # sku=sku,
  34. # defaults={
  35. # 'title': product_data.get('title', ''),
  36. # 'description': product_data.get('description', ''),
  37. # 'category': category,
  38. # 'attributes': product_data.get('attributes', {})
  39. # }
  40. # )
  41. # # Update if exists
  42. # if not created:
  43. # product.title = product_data.get('title', product.title)
  44. # product.description = product_data.get('description', product.description)
  45. # product.attributes = product_data.get('attributes', product.attributes)
  46. # product.save()
  47. # # Get rules (cached)
  48. # cache_key = f"category_rules_{category}"
  49. # rules = cache.get(cache_key)
  50. # if rules is None:
  51. # rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  52. # cache.set(cache_key, rules, 3600)
  53. # if not rules:
  54. # return JsonResponse({'error': f'No rules defined for {category}'}, status=400)
  55. # # Force AI suggestions
  56. # score_result = self.scorer.score_product(
  57. # {
  58. # 'sku': product.sku,
  59. # 'category': product.category,
  60. # 'title': product.title,
  61. # 'description': product.description,
  62. # 'attributes': product.attributes
  63. # },
  64. # rules,
  65. # generate_ai_suggestions=True # always generate AI
  66. # )
  67. # # Save score
  68. # AttributeScore.objects.create(
  69. # product=product,
  70. # score=score_result['final_score'],
  71. # max_score=score_result['max_score'],
  72. # details=score_result['breakdown'],
  73. # issues=score_result['issues'],
  74. # suggestions=score_result['suggestions'],
  75. # ai_suggestions=score_result.get('ai_suggestions', {}),
  76. # processing_time=score_result.get('processing_time', 0)
  77. # )
  78. # return JsonResponse({
  79. # 'success': True,
  80. # 'product_sku': sku,
  81. # 'created': created,
  82. # 'score_result': score_result
  83. # })
  84. # except json.JSONDecodeError:
  85. # return JsonResponse({'error': 'Invalid JSON'}, status=400)
  86. # except Exception as e:
  87. # logger.error(f"Error scoring product: {str(e)}", exc_info=True)
  88. # return JsonResponse({'error': str(e)}, status=500)
  89. # from django.views import View
  90. # from django.http import JsonResponse
  91. # from django.utils.decorators import method_decorator
  92. # from django.views.decorators.csrf import csrf_exempt
  93. # import json
  94. # import logging
  95. # from .models import Product, CategoryAttributeRule, AttributeScore
  96. # from .services.attribute_scorer import AttributeQualityScorer
  97. # logger = logging.getLogger(__name__)
  98. # @method_decorator(csrf_exempt, name='dispatch')
  99. # class BatchScoreView(View):
  100. # """Batch scoring with AI suggestions"""
  101. # def __init__(self, *args, **kwargs):
  102. # super().__init__(*args, **kwargs)
  103. # self.scorer = AttributeQualityScorer(use_ai=True) # enable AI even for batch
  104. # def post(self, request):
  105. # try:
  106. # data = json.loads(request.body)
  107. # products = data.get('products', [])
  108. # if not products:
  109. # return JsonResponse({'error': 'No products provided'}, status=400)
  110. # results = []
  111. # errors = []
  112. # for product_data in products[:100]: # limit 100
  113. # try:
  114. # sku = product_data.get('sku')
  115. # category = product_data.get('category')
  116. # if not sku or not category:
  117. # errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  118. # continue
  119. # # Get rules
  120. # rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  121. # if not rules:
  122. # errors.append({'sku': sku, 'error': f'No rules for category {category}'})
  123. # continue
  124. # # Force AI suggestions
  125. # score_result = self.scorer.score_product(
  126. # product_data,
  127. # rules,
  128. # generate_ai_suggestions=True # <- key change
  129. # )
  130. # results.append({
  131. # 'sku': sku,
  132. # 'final_score': score_result['final_score'],
  133. # 'max_score': score_result['max_score'],
  134. # 'breakdown': score_result['breakdown'],
  135. # 'issues': score_result['issues'],
  136. # 'suggestions': score_result['suggestions'],
  137. # 'ai_suggestions': score_result.get('ai_suggestions', {}),
  138. # 'processing_time': score_result.get('processing_time', 0)
  139. # })
  140. # except Exception as e:
  141. # errors.append({'sku': product_data.get('sku'), 'error': str(e)})
  142. # return JsonResponse({
  143. # 'success': True,
  144. # 'processed': len(results),
  145. # 'results': results,
  146. # 'errors': errors
  147. # })
  148. # except Exception as e:
  149. # logger.error(f"Batch scoring error: {str(e)}")
  150. # return JsonResponse({'error': str(e)}, status=500)
  151. # # views.py (Enhanced with ProductContentRule support - FIXED)
  152. # from django.shortcuts import render, get_object_or_404
  153. # from django.http import JsonResponse
  154. # from django.views import View
  155. # from django.core.cache import cache
  156. # from django.db.models import Q # ← FIXED: Import Q from django.db.models
  157. # from django.views.decorators.csrf import csrf_exempt
  158. # from django.utils.decorators import method_decorator
  159. # import json
  160. # import logging
  161. # from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product
  162. # from core.services.attribute_scorer import AttributeQualityScorer
  163. # logger = logging.getLogger(__name__)
  164. # @method_decorator(csrf_exempt, name='dispatch')
  165. # class AttributeScoreView(View):
  166. # """Enhanced API view with ProductContentRule support"""
  167. # def __init__(self, *args, **kwargs):
  168. # super().__init__(*args, **kwargs)
  169. # self.scorer = AttributeQualityScorer(use_ai=True)
  170. # def post(self, request, *args, **kwargs):
  171. # """Score a single product with AI suggestions and content rules validation"""
  172. # try:
  173. # data = json.loads(request.body)
  174. # product_data = data.get('product', {})
  175. # sku = product_data.get('sku')
  176. # use_ai = data.get('use_ai', True)
  177. # if not sku:
  178. # return JsonResponse({'error': 'SKU is required'}, status=400)
  179. # category = product_data.get('category', '')
  180. # if not category:
  181. # return JsonResponse({'error': 'Category is required'}, status=400)
  182. # # Get or create product
  183. # product, created = Product.objects.get_or_create(
  184. # sku=sku,
  185. # defaults={
  186. # 'title': product_data.get('title', ''),
  187. # 'description': product_data.get('description', ''),
  188. # 'short_description': product_data.get('short_description', ''),
  189. # 'seo_title': product_data.get('seo_title', ''),
  190. # 'seo_description': product_data.get('seo_description', ''),
  191. # 'category': category,
  192. # 'attributes': product_data.get('attributes', {})
  193. # }
  194. # )
  195. # # Update if exists
  196. # if not created:
  197. # product.title = product_data.get('title', product.title)
  198. # product.description = product_data.get('description', product.description)
  199. # product.short_description = product_data.get('short_description', product.short_description)
  200. # product.seo_title = product_data.get('seo_title', product.seo_title)
  201. # product.seo_description = product_data.get('seo_description', product.seo_description)
  202. # product.attributes = product_data.get('attributes', product.attributes)
  203. # product.save()
  204. # # Get CategoryAttributeRules (cached)
  205. # cache_key = f"category_rules_{category}"
  206. # category_rules = cache.get(cache_key)
  207. # if category_rules is None:
  208. # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  209. # cache.set(cache_key, category_rules, 3600)
  210. # if not category_rules:
  211. # return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400)
  212. # # Get ProductContentRules (cached) - FIXED: Use Q from django.db.models
  213. # content_cache_key = f"content_rules_{category}"
  214. # content_rules = cache.get(content_cache_key)
  215. # if content_rules is None:
  216. # # Get both global rules (category=None) and category-specific rules
  217. # content_rules = list(
  218. # ProductContentRule.objects.filter(
  219. # Q(category__isnull=True) | Q(category=category)
  220. # ).values()
  221. # )
  222. # cache.set(content_cache_key, content_rules, 3600)
  223. # # Build product dict with all fields
  224. # product_dict = {
  225. # 'sku': product.sku,
  226. # 'category': product.category,
  227. # 'title': product.title,
  228. # 'description': product.description,
  229. # 'short_description': product.short_description,
  230. # 'seo_title': product.seo_title,
  231. # 'seo_description': product.seo_description,
  232. # 'attributes': product.attributes
  233. # }
  234. # # Score product with content rules
  235. # score_result = self.scorer.score_product(
  236. # product_dict,
  237. # category_rules,
  238. # content_rules=content_rules,
  239. # generate_ai_suggestions=True
  240. # )
  241. # # Save score
  242. # AttributeScore.objects.create(
  243. # product=product,
  244. # score=score_result['final_score'],
  245. # max_score=score_result['max_score'],
  246. # details=score_result['breakdown'],
  247. # issues=score_result['issues'],
  248. # suggestions=score_result['suggestions'],
  249. # ai_suggestions=score_result.get('ai_suggestions', {}),
  250. # processing_time=score_result.get('processing_time', 0)
  251. # )
  252. # return JsonResponse({
  253. # 'success': True,
  254. # 'product_sku': sku,
  255. # 'created': created,
  256. # 'score_result': score_result
  257. # })
  258. # except json.JSONDecodeError:
  259. # return JsonResponse({'error': 'Invalid JSON'}, status=400)
  260. # except Exception as e:
  261. # logger.error(f"Error scoring product: {str(e)}", exc_info=True)
  262. # return JsonResponse({'error': str(e)}, status=500)
  263. # @method_decorator(csrf_exempt, name='dispatch')
  264. # class BatchScoreView(View):
  265. # """Batch scoring with AI suggestions and content rules"""
  266. # def __init__(self, *args, **kwargs):
  267. # super().__init__(*args, **kwargs)
  268. # self.scorer = AttributeQualityScorer(use_ai=True)
  269. # def post(self, request):
  270. # try:
  271. # data = json.loads(request.body)
  272. # products = data.get('products', [])
  273. # if not products:
  274. # return JsonResponse({'error': 'No products provided'}, status=400)
  275. # results = []
  276. # errors = []
  277. # for product_data in products[:100]: # limit 100
  278. # try:
  279. # sku = product_data.get('sku')
  280. # category = product_data.get('category')
  281. # if not sku or not category:
  282. # errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  283. # continue
  284. # # Get attribute rules
  285. # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  286. # if not category_rules:
  287. # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  288. # continue
  289. # # Get content rules - FIXED: Use Q from django.db.models
  290. # content_rules = list(
  291. # ProductContentRule.objects.filter(
  292. # Q(category__isnull=True) | Q(category=category)
  293. # ).values()
  294. # )
  295. # # Score with content rules
  296. # score_result = self.scorer.score_product(
  297. # product_data,
  298. # category_rules,
  299. # content_rules=content_rules,
  300. # generate_ai_suggestions=True
  301. # )
  302. # results.append({
  303. # 'sku': sku,
  304. # 'final_score': score_result['final_score'],
  305. # 'max_score': score_result['max_score'],
  306. # 'breakdown': score_result['breakdown'],
  307. # 'issues': score_result['issues'],
  308. # 'suggestions': score_result['suggestions'],
  309. # 'ai_suggestions': score_result.get('ai_suggestions', {}),
  310. # 'processing_time': score_result.get('processing_time', 0)
  311. # })
  312. # except Exception as e:
  313. # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True)
  314. # errors.append({'sku': product_data.get('sku'), 'error': str(e)})
  315. # return JsonResponse({
  316. # 'success': True,
  317. # 'processed': len(results),
  318. # 'results': results,
  319. # 'errors': errors
  320. # })
  321. # except Exception as e:
  322. # logger.error(f"Batch scoring error: {str(e)}", exc_info=True)
  323. # return JsonResponse({'error': str(e)}, status=500)
  324. # @method_decorator(csrf_exempt, name='dispatch')
  325. # class ContentRulesView(View):
  326. # """API to manage ProductContentRules"""
  327. # def get(self, request):
  328. # """Get all content rules, optionally filtered by category"""
  329. # try:
  330. # category = request.GET.get('category')
  331. # if category:
  332. # # FIXED: Use Q from django.db.models
  333. # rules = ProductContentRule.objects.filter(
  334. # Q(category__isnull=True) | Q(category=category)
  335. # )
  336. # else:
  337. # rules = ProductContentRule.objects.all()
  338. # rules_data = list(rules.values())
  339. # return JsonResponse({
  340. # 'success': True,
  341. # 'count': len(rules_data),
  342. # 'rules': rules_data
  343. # })
  344. # except Exception as e:
  345. # logger.error(f"Error fetching content rules: {e}", exc_info=True)
  346. # return JsonResponse({'error': str(e)}, status=500)
  347. # def post(self, request):
  348. # """Create a new content rule"""
  349. # try:
  350. # data = json.loads(request.body)
  351. # required_fields = ['field_name']
  352. # if not all(field in data for field in required_fields):
  353. # return JsonResponse({'error': 'field_name is required'}, status=400)
  354. # # Create rule
  355. # rule = ProductContentRule.objects.create(
  356. # category=data.get('category'),
  357. # field_name=data['field_name'],
  358. # is_mandatory=data.get('is_mandatory', True),
  359. # min_length=data.get('min_length'),
  360. # max_length=data.get('max_length'),
  361. # min_word_count=data.get('min_word_count'),
  362. # max_word_count=data.get('max_word_count'),
  363. # must_contain_keywords=data.get('must_contain_keywords', []),
  364. # validation_regex=data.get('validation_regex', ''),
  365. # description=data.get('description', '')
  366. # )
  367. # # Clear cache
  368. # if data.get('category'):
  369. # cache.delete(f"content_rules_{data['category']}")
  370. # return JsonResponse({
  371. # 'success': True,
  372. # 'rule_id': rule.id,
  373. # 'message': 'Content rule created successfully'
  374. # })
  375. # except Exception as e:
  376. # logger.error(f"Error creating content rule: {e}", exc_info=True)
  377. # return JsonResponse({'error': str(e)}, status=500)
  378. # @method_decorator(csrf_exempt, name='dispatch')
  379. # class ProductScoreDetailView(View):
  380. # """Get detailed score for a specific product"""
  381. # def get(self, request, sku):
  382. # try:
  383. # product = get_object_or_404(Product, sku=sku)
  384. # # Get latest score
  385. # latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first()
  386. # if not latest_score:
  387. # return JsonResponse({'error': 'No score found for this product'}, status=404)
  388. # # Get interpretation
  389. # scorer = AttributeQualityScorer()
  390. # interpretation = scorer.get_score_interpretation(latest_score.score)
  391. # return JsonResponse({
  392. # 'success': True,
  393. # 'product': {
  394. # 'sku': product.sku,
  395. # 'category': product.category,
  396. # 'title': product.title,
  397. # 'description': product.description,
  398. # 'short_description': product.short_description,
  399. # 'seo_title': product.seo_title,
  400. # 'seo_description': product.seo_description,
  401. # 'attributes': product.attributes
  402. # },
  403. # 'score': {
  404. # 'final_score': latest_score.score,
  405. # 'max_score': latest_score.max_score,
  406. # 'breakdown': latest_score.details,
  407. # 'interpretation': interpretation
  408. # },
  409. # 'issues': latest_score.issues,
  410. # 'suggestions': latest_score.suggestions,
  411. # 'ai_suggestions': latest_score.ai_suggestions,
  412. # 'scored_at': latest_score.created_at.isoformat()
  413. # })
  414. # except Exception as e:
  415. # logger.error(f"Error fetching product score: {e}", exc_info=True)
  416. # return JsonResponse({'error': str(e)}, status=500)
  417. # # views.py (Enhanced with categorized issues and suggestions)
  418. # from django.shortcuts import render, get_object_or_404
  419. # from django.http import JsonResponse
  420. # from django.views import View
  421. # from django.core.cache import cache
  422. # from django.db.models import Q
  423. # from django.views.decorators.csrf import csrf_exempt
  424. # from django.utils.decorators import method_decorator
  425. # import json
  426. # import logging
  427. # from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product
  428. # from core.services.attribute_scorer import AttributeQualityScorer
  429. # logger = logging.getLogger(__name__)
  430. # def categorize_issues_and_suggestions(issues, suggestions):
  431. # """
  432. # Categorize issues and suggestions by component (attributes, title, description, seo)
  433. # Returns a structured dict with categorized items
  434. # """
  435. # categorized = {
  436. # 'attributes': {'issues': [], 'suggestions': []},
  437. # 'title': {'issues': [], 'suggestions': []},
  438. # 'description': {'issues': [], 'suggestions': []},
  439. # 'seo': {'issues': [], 'suggestions': []},
  440. # 'content_rules': {'issues': [], 'suggestions': []},
  441. # 'general': {'issues': [], 'suggestions': []}
  442. # }
  443. # # Categorize issues
  444. # for issue in issues:
  445. # issue_lower = issue.lower()
  446. # if issue.startswith('Title:'):
  447. # categorized['title']['issues'].append(issue.replace('Title:', '').strip())
  448. # elif issue.startswith('Description:'):
  449. # categorized['description']['issues'].append(issue.replace('Description:', '').strip())
  450. # elif issue.startswith('SEO:'):
  451. # categorized['seo']['issues'].append(issue.replace('SEO:', '').strip())
  452. # elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  453. # categorized['seo']['issues'].append(issue)
  454. # elif 'content rule' in issue_lower or any(field in issue for field in ['Seo Title:', 'Seo Description:']):
  455. # categorized['content_rules']['issues'].append(issue)
  456. # elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']):
  457. # categorized['attributes']['issues'].append(issue)
  458. # else:
  459. # categorized['general']['issues'].append(issue)
  460. # # Categorize suggestions
  461. # for suggestion in suggestions:
  462. # suggestion_lower = suggestion.lower()
  463. # # Check for explicit component prefixes
  464. # if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']):
  465. # categorized['title']['suggestions'].append(suggestion)
  466. # elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']):
  467. # categorized['description']['suggestions'].append(suggestion)
  468. # elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']):
  469. # categorized['seo']['suggestions'].append(suggestion)
  470. # elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description', 'add seo']):
  471. # categorized['seo']['suggestions'].append(suggestion)
  472. # elif 'content rule' in suggestion_lower:
  473. # categorized['content_rules']['suggestions'].append(suggestion)
  474. # elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize', 'correct capitalization']):
  475. # categorized['attributes']['suggestions'].append(suggestion)
  476. # else:
  477. # categorized['general']['suggestions'].append(suggestion)
  478. # # Remove empty categories
  479. # return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']}
  480. # @method_decorator(csrf_exempt, name='dispatch')
  481. # class AttributeScoreView(View):
  482. # """Enhanced API view with ProductContentRule support"""
  483. # def __init__(self, *args, **kwargs):
  484. # super().__init__(*args, **kwargs)
  485. # self.scorer = AttributeQualityScorer(use_ai=True)
  486. # def post(self, request, *args, **kwargs):
  487. # """Score a single product with AI suggestions and content rules validation"""
  488. # try:
  489. # data = json.loads(request.body)
  490. # product_data = data.get('product', {})
  491. # sku = product_data.get('sku')
  492. # use_ai = data.get('use_ai', True)
  493. # if not sku:
  494. # return JsonResponse({'error': 'SKU is required'}, status=400)
  495. # category = product_data.get('category', '')
  496. # if not category:
  497. # return JsonResponse({'error': 'Category is required'}, status=400)
  498. # # Get or create product
  499. # product, created = Product.objects.get_or_create(
  500. # sku=sku,
  501. # defaults={
  502. # 'title': product_data.get('title', ''),
  503. # 'description': product_data.get('description', ''),
  504. # 'short_description': product_data.get('short_description', ''),
  505. # 'seo_title': product_data.get('seo_title', ''),
  506. # 'seo_description': product_data.get('seo_description', ''),
  507. # 'category': category,
  508. # 'attributes': product_data.get('attributes', {})
  509. # }
  510. # )
  511. # # Update if exists
  512. # if not created:
  513. # product.title = product_data.get('title', product.title)
  514. # product.description = product_data.get('description', product.description)
  515. # product.short_description = product_data.get('short_description', product.short_description)
  516. # product.seo_title = product_data.get('seo_title', product.seo_title)
  517. # product.seo_description = product_data.get('seo_description', product.seo_description)
  518. # product.attributes = product_data.get('attributes', product.attributes)
  519. # product.save()
  520. # # Get CategoryAttributeRules (cached)
  521. # cache_key = f"category_rules_{category}"
  522. # category_rules = cache.get(cache_key)
  523. # if category_rules is None:
  524. # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  525. # cache.set(cache_key, category_rules, 3600)
  526. # if not category_rules:
  527. # return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400)
  528. # # Get ProductContentRules (cached)
  529. # content_cache_key = f"content_rules_{category}"
  530. # content_rules = cache.get(content_cache_key)
  531. # if content_rules is None:
  532. # content_rules = list(
  533. # ProductContentRule.objects.filter(
  534. # Q(category__isnull=True) | Q(category=category)
  535. # ).values()
  536. # )
  537. # cache.set(content_cache_key, content_rules, 3600)
  538. # # Build product dict with all fields
  539. # product_dict = {
  540. # 'sku': product.sku,
  541. # 'category': product.category,
  542. # 'title': product.title,
  543. # 'description': product.description,
  544. # 'short_description': product.short_description,
  545. # 'seo_title': product.seo_title,
  546. # 'seo_description': product.seo_description,
  547. # 'attributes': product.attributes
  548. # }
  549. # # Score product with content rules
  550. # score_result = self.scorer.score_product(
  551. # product_dict,
  552. # category_rules,
  553. # content_rules=content_rules,
  554. # generate_ai_suggestions=True
  555. # )
  556. # # Categorize issues and suggestions
  557. # categorized = categorize_issues_and_suggestions(
  558. # score_result['issues'],
  559. # score_result['suggestions']
  560. # )
  561. # # Save score
  562. # AttributeScore.objects.create(
  563. # product=product,
  564. # score=score_result['final_score'],
  565. # max_score=score_result['max_score'],
  566. # details=score_result['breakdown'],
  567. # issues=score_result['issues'],
  568. # suggestions=score_result['suggestions'],
  569. # ai_suggestions=score_result.get('ai_suggestions', {}),
  570. # processing_time=score_result.get('processing_time', 0)
  571. # )
  572. # # Enhanced response with categorized issues
  573. # return JsonResponse({
  574. # 'success': True,
  575. # 'product_sku': sku,
  576. # 'created': created,
  577. # 'score_result': {
  578. # 'final_score': score_result['final_score'],
  579. # 'max_score': score_result['max_score'],
  580. # 'breakdown': score_result['breakdown'],
  581. # 'categorized_feedback': categorized, # NEW: Categorized issues/suggestions
  582. # 'ai_suggestions': score_result.get('ai_suggestions', {}),
  583. # 'processing_time': score_result.get('processing_time', 0),
  584. # # Keep original format for backward compatibility
  585. # 'issues': score_result['issues'],
  586. # 'suggestions': score_result['suggestions']
  587. # }
  588. # })
  589. # except json.JSONDecodeError:
  590. # return JsonResponse({'error': 'Invalid JSON'}, status=400)
  591. # except Exception as e:
  592. # logger.error(f"Error scoring product: {str(e)}", exc_info=True)
  593. # return JsonResponse({'error': str(e)}, status=500)
  594. # @method_decorator(csrf_exempt, name='dispatch')
  595. # class BatchScoreView(View):
  596. # """Batch scoring with AI suggestions and content rules"""
  597. # def __init__(self, *args, **kwargs):
  598. # super().__init__(*args, **kwargs)
  599. # self.scorer = AttributeQualityScorer(use_ai=True)
  600. # def post(self, request):
  601. # try:
  602. # data = json.loads(request.body)
  603. # products = data.get('products', [])
  604. # if not products:
  605. # return JsonResponse({'error': 'No products provided'}, status=400)
  606. # results = []
  607. # errors = []
  608. # for product_data in products[:100]: # limit 100
  609. # try:
  610. # sku = product_data.get('sku')
  611. # category = product_data.get('category')
  612. # if not sku or not category:
  613. # errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  614. # continue
  615. # # Get attribute rules
  616. # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  617. # if not category_rules:
  618. # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  619. # continue
  620. # # Get content rules
  621. # content_rules = list(
  622. # ProductContentRule.objects.filter(
  623. # Q(category__isnull=True) | Q(category=category)
  624. # ).values()
  625. # )
  626. # # Score with content rules
  627. # score_result = self.scorer.score_product(
  628. # product_data,
  629. # category_rules,
  630. # content_rules=content_rules,
  631. # generate_ai_suggestions=True
  632. # )
  633. # # Categorize issues and suggestions
  634. # categorized = categorize_issues_and_suggestions(
  635. # score_result['issues'],
  636. # score_result['suggestions']
  637. # )
  638. # results.append({
  639. # 'sku': sku,
  640. # 'final_score': score_result['final_score'],
  641. # 'max_score': score_result['max_score'],
  642. # 'breakdown': score_result['breakdown'],
  643. # 'categorized_feedback': categorized, # NEW: Categorized by component
  644. # 'ai_suggestions': score_result.get('ai_suggestions', {}),
  645. # 'processing_time': score_result.get('processing_time', 0),
  646. # # Keep original format for backward compatibility
  647. # 'issues': score_result['issues'],
  648. # 'suggestions': score_result['suggestions']
  649. # })
  650. # except Exception as e:
  651. # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True)
  652. # errors.append({'sku': product_data.get('sku'), 'error': str(e)})
  653. # return JsonResponse({
  654. # 'success': True,
  655. # 'processed': len(results),
  656. # 'results': results,
  657. # 'errors': errors
  658. # })
  659. # except Exception as e:
  660. # logger.error(f"Batch scoring error: {str(e)}", exc_info=True)
  661. # return JsonResponse({'error': str(e)}, status=500)
  662. # @method_decorator(csrf_exempt, name='dispatch')
  663. # class ContentRulesView(View):
  664. # """API to manage ProductContentRules"""
  665. # def get(self, request):
  666. # """Get all content rules, optionally filtered by category"""
  667. # try:
  668. # category = request.GET.get('category')
  669. # if category:
  670. # rules = ProductContentRule.objects.filter(
  671. # Q(category__isnull=True) | Q(category=category)
  672. # )
  673. # else:
  674. # rules = ProductContentRule.objects.all()
  675. # rules_data = list(rules.values())
  676. # return JsonResponse({
  677. # 'success': True,
  678. # 'count': len(rules_data),
  679. # 'rules': rules_data
  680. # })
  681. # except Exception as e:
  682. # logger.error(f"Error fetching content rules: {e}", exc_info=True)
  683. # return JsonResponse({'error': str(e)}, status=500)
  684. # def post(self, request):
  685. # """Create a new content rule"""
  686. # try:
  687. # data = json.loads(request.body)
  688. # required_fields = ['field_name']
  689. # if not all(field in data for field in required_fields):
  690. # return JsonResponse({'error': 'field_name is required'}, status=400)
  691. # # Create rule
  692. # rule = ProductContentRule.objects.create(
  693. # category=data.get('category'),
  694. # field_name=data['field_name'],
  695. # is_mandatory=data.get('is_mandatory', True),
  696. # min_length=data.get('min_length'),
  697. # max_length=data.get('max_length'),
  698. # min_word_count=data.get('min_word_count'),
  699. # max_word_count=data.get('max_word_count'),
  700. # must_contain_keywords=data.get('must_contain_keywords', []),
  701. # validation_regex=data.get('validation_regex', ''),
  702. # description=data.get('description', '')
  703. # )
  704. # # Clear cache
  705. # if data.get('category'):
  706. # cache.delete(f"content_rules_{data['category']}")
  707. # return JsonResponse({
  708. # 'success': True,
  709. # 'rule_id': rule.id,
  710. # 'message': 'Content rule created successfully'
  711. # })
  712. # except Exception as e:
  713. # logger.error(f"Error creating content rule: {e}", exc_info=True)
  714. # return JsonResponse({'error': str(e)}, status=500)
  715. # @method_decorator(csrf_exempt, name='dispatch')
  716. # class ProductScoreDetailView(View):
  717. # """Get detailed score for a specific product"""
  718. # def get(self, request, sku):
  719. # try:
  720. # product = get_object_or_404(Product, sku=sku)
  721. # # Get latest score
  722. # latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first()
  723. # if not latest_score:
  724. # return JsonResponse({'error': 'No score found for this product'}, status=404)
  725. # # Get interpretation
  726. # scorer = AttributeQualityScorer()
  727. # interpretation = scorer.get_score_interpretation(latest_score.score)
  728. # # Categorize issues and suggestions
  729. # categorized = categorize_issues_and_suggestions(
  730. # latest_score.issues,
  731. # latest_score.suggestions
  732. # )
  733. # return JsonResponse({
  734. # 'success': True,
  735. # 'product': {
  736. # 'sku': product.sku,
  737. # 'category': product.category,
  738. # 'title': product.title,
  739. # 'description': product.description,
  740. # 'short_description': product.short_description,
  741. # 'seo_title': product.seo_title,
  742. # 'seo_description': product.seo_description,
  743. # 'attributes': product.attributes
  744. # },
  745. # 'score': {
  746. # 'final_score': latest_score.score,
  747. # 'max_score': latest_score.max_score,
  748. # 'breakdown': latest_score.details,
  749. # 'interpretation': interpretation
  750. # },
  751. # 'categorized_feedback': categorized, # NEW: Categorized by component
  752. # 'ai_suggestions': latest_score.ai_suggestions,
  753. # 'scored_at': latest_score.created_at.isoformat(),
  754. # # Keep original format for backward compatibility
  755. # 'issues': latest_score.issues,
  756. # 'suggestions': latest_score.suggestions
  757. # })
  758. # except Exception as e:
  759. # logger.error(f"Error fetching product score: {e}", exc_info=True)
  760. # return JsonResponse({'error': str(e)}, status=500)
  761. # views.py (Enhanced with categorized issues and suggestions)
  762. from django.shortcuts import render, get_object_or_404
  763. from django.http import JsonResponse
  764. from django.views import View
  765. from django.core.cache import cache
  766. from django.db.models import Q
  767. from django.views.decorators.csrf import csrf_exempt
  768. from django.utils.decorators import method_decorator
  769. import json
  770. import logging
  771. from core.services.image_scorer import ImageQualityScorer
  772. from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product
  773. from core.services.attribute_scorer import AttributeQualityScorer
  774. logger = logging.getLogger(__name__)
  775. def categorize_ai_improvements(ai_suggestions):
  776. """
  777. Consolidate AI improvements by component
  778. Returns a structured dict with grouped improvements per component
  779. """
  780. if not ai_suggestions or 'improvements' not in ai_suggestions:
  781. return None
  782. grouped = {
  783. 'attributes': [],
  784. 'title': [],
  785. 'description': [],
  786. 'seo': [],
  787. 'general': []
  788. }
  789. for improvement in ai_suggestions.get('improvements', []):
  790. component = improvement.get('component', 'general').lower()
  791. # Normalize component names
  792. if component in grouped:
  793. grouped[component].append({
  794. 'issue': improvement.get('issue', ''),
  795. 'suggestion': improvement.get('suggestion', ''),
  796. 'priority': improvement.get('priority', 'medium'),
  797. 'confidence': improvement.get('confidence', 'medium')
  798. })
  799. else:
  800. grouped['general'].append({
  801. 'issue': improvement.get('issue', ''),
  802. 'suggestion': improvement.get('suggestion', ''),
  803. 'priority': improvement.get('priority', 'medium'),
  804. 'confidence': improvement.get('confidence', 'medium')
  805. })
  806. # Remove empty categories
  807. grouped = {k: v for k, v in grouped.items() if v}
  808. return grouped
  809. def categorize_issues_and_suggestions(issues, suggestions):
  810. """
  811. Categorize issues and suggestions by component (attributes, title, description, seo)
  812. Returns a structured dict with categorized items
  813. """
  814. categorized = {
  815. 'attributes': {'issues': [], 'suggestions': []},
  816. 'title': {'issues': [], 'suggestions': []},
  817. 'description': {'issues': [], 'suggestions': []},
  818. 'seo': {'issues': [], 'suggestions': []},
  819. 'content_rules': {'issues': [], 'suggestions': []},
  820. 'general': {'issues': [], 'suggestions': []}
  821. }
  822. # Categorize issues
  823. for issue in issues:
  824. issue_lower = issue.lower()
  825. if issue.startswith('Title:'):
  826. categorized['title']['issues'].append(issue.replace('Title:', '').strip())
  827. elif issue.startswith('Description:'):
  828. categorized['description']['issues'].append(issue.replace('Description:', '').strip())
  829. elif issue.startswith('SEO:'):
  830. categorized['seo']['issues'].append(issue.replace('SEO:', '').strip())
  831. elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']):
  832. categorized['seo']['issues'].append(issue)
  833. elif 'content rule' in issue_lower or any(field in issue for field in ['Seo Title:', 'Seo Description:']):
  834. categorized['content_rules']['issues'].append(issue)
  835. elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']):
  836. categorized['attributes']['issues'].append(issue)
  837. else:
  838. categorized['general']['issues'].append(issue)
  839. # Categorize suggestions
  840. for suggestion in suggestions:
  841. suggestion_lower = suggestion.lower()
  842. # Check for explicit component prefixes
  843. if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']):
  844. categorized['title']['suggestions'].append(suggestion)
  845. elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']):
  846. categorized['description']['suggestions'].append(suggestion)
  847. elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']):
  848. categorized['seo']['suggestions'].append(suggestion)
  849. elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description', 'add seo']):
  850. categorized['seo']['suggestions'].append(suggestion)
  851. elif 'content rule' in suggestion_lower:
  852. categorized['content_rules']['suggestions'].append(suggestion)
  853. elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize', 'correct capitalization']):
  854. categorized['attributes']['suggestions'].append(suggestion)
  855. else:
  856. categorized['general']['suggestions'].append(suggestion)
  857. # Remove empty categories
  858. return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']}
  859. @method_decorator(csrf_exempt, name='dispatch')
  860. class AttributeScoreView(View):
  861. """Enhanced API view with ProductContentRule support"""
  862. def __init__(self, *args, **kwargs):
  863. super().__init__(*args, **kwargs)
  864. self.scorer = AttributeQualityScorer(use_ai=True)
  865. def post(self, request, *args, **kwargs):
  866. """Score a single product with AI suggestions and content rules validation"""
  867. try:
  868. data = json.loads(request.body)
  869. product_data = data.get('product', {})
  870. sku = product_data.get('sku')
  871. use_ai = data.get('use_ai', True)
  872. if not sku:
  873. return JsonResponse({'error': 'SKU is required'}, status=400)
  874. category = product_data.get('category', '')
  875. if not category:
  876. return JsonResponse({'error': 'Category is required'}, status=400)
  877. # Get or create product
  878. product, created = Product.objects.get_or_create(
  879. sku=sku,
  880. defaults={
  881. 'title': product_data.get('title', ''),
  882. 'description': product_data.get('description', ''),
  883. 'short_description': product_data.get('short_description', ''),
  884. 'seo_title': product_data.get('seo_title', ''),
  885. 'seo_description': product_data.get('seo_description', ''),
  886. 'category': category,
  887. 'attributes': product_data.get('attributes', {})
  888. }
  889. )
  890. # Update if exists
  891. if not created:
  892. product.title = product_data.get('title', product.title)
  893. product.description = product_data.get('description', product.description)
  894. product.short_description = product_data.get('short_description', product.short_description)
  895. product.seo_title = product_data.get('seo_title', product.seo_title)
  896. product.seo_description = product_data.get('seo_description', product.seo_description)
  897. product.attributes = product_data.get('attributes', product.attributes)
  898. product.save()
  899. # Get CategoryAttributeRules (cached)
  900. cache_key = f"category_rules_{category}"
  901. category_rules = cache.get(cache_key)
  902. if category_rules is None:
  903. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  904. cache.set(cache_key, category_rules, 3600)
  905. if not category_rules:
  906. return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400)
  907. # Get ProductContentRules (cached)
  908. content_cache_key = f"content_rules_{category}"
  909. content_rules = cache.get(content_cache_key)
  910. if content_rules is None:
  911. content_rules = list(
  912. ProductContentRule.objects.filter(
  913. Q(category__isnull=True) | Q(category=category)
  914. ).values()
  915. )
  916. cache.set(content_cache_key, content_rules, 3600)
  917. # Build product dict with all fields
  918. product_dict = {
  919. 'sku': product.sku,
  920. 'category': product.category,
  921. 'title': product.title,
  922. 'description': product.description,
  923. 'short_description': product.short_description,
  924. 'seo_title': product.seo_title,
  925. 'seo_description': product.seo_description,
  926. 'attributes': product.attributes
  927. }
  928. # Score product with content rules
  929. score_result = self.scorer.score_product(
  930. product_dict,
  931. category_rules,
  932. content_rules=content_rules,
  933. generate_ai_suggestions=True
  934. )
  935. # Categorize issues and suggestions
  936. categorized = categorize_issues_and_suggestions(
  937. score_result['issues'],
  938. score_result['suggestions']
  939. )
  940. # Save score
  941. AttributeScore.objects.create(
  942. product=product,
  943. score=score_result['final_score'],
  944. max_score=score_result['max_score'],
  945. details=score_result['breakdown'],
  946. issues=score_result['issues'],
  947. suggestions=score_result['suggestions'],
  948. ai_suggestions=score_result.get('ai_suggestions', {}),
  949. processing_time=score_result.get('processing_time', 0)
  950. )
  951. # Enhanced response with categorized issues
  952. return JsonResponse({
  953. 'success': True,
  954. 'product_sku': sku,
  955. 'created': created,
  956. 'score_result': {
  957. 'final_score': score_result['final_score'],
  958. 'max_score': score_result['max_score'],
  959. 'breakdown': score_result['breakdown'],
  960. 'categorized_feedback': categorized, # NEW: Categorized issues/suggestions
  961. 'ai_suggestions': score_result.get('ai_suggestions', {}),
  962. 'processing_time': score_result.get('processing_time', 0),
  963. # Keep original format for backward compatibility
  964. 'issues': score_result['issues'],
  965. 'suggestions': score_result['suggestions']
  966. }
  967. })
  968. except json.JSONDecodeError:
  969. return JsonResponse({'error': 'Invalid JSON'}, status=400)
  970. except Exception as e:
  971. logger.error(f"Error scoring product: {str(e)}", exc_info=True)
  972. return JsonResponse({'error': str(e)}, status=500)
  973. # @method_decorator(csrf_exempt, name='dispatch')
  974. # class BatchScoreView(View):
  975. # """Batch scoring with AI suggestions and content rules"""
  976. # def __init__(self, *args, **kwargs):
  977. # super().__init__(*args, **kwargs)
  978. # self.scorer = AttributeQualityScorer(use_ai=True)
  979. # def post(self, request):
  980. # try:
  981. # data = json.loads(request.body)
  982. # products = data.get('products', [])
  983. # if not products:
  984. # return JsonResponse({'error': 'No products provided'}, status=400)
  985. # results = []
  986. # errors = []
  987. # for product_data in products[:100]: # limit 100
  988. # try:
  989. # sku = product_data.get('sku')
  990. # category = product_data.get('category')
  991. # if not sku or not category:
  992. # errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  993. # continue
  994. # # Get attribute rules
  995. # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  996. # if not category_rules:
  997. # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  998. # continue
  999. # # Get content rules
  1000. # content_rules = list(
  1001. # ProductContentRule.objects.filter(
  1002. # Q(category__isnull=True) | Q(category=category)
  1003. # ).values()
  1004. # )
  1005. # # Score with content rules
  1006. # score_result = self.scorer.score_product(
  1007. # product_data,
  1008. # category_rules,
  1009. # content_rules=content_rules,
  1010. # generate_ai_suggestions=True
  1011. # )
  1012. # # Categorize issues and suggestions
  1013. # categorized = categorize_issues_and_suggestions(
  1014. # score_result['issues'],
  1015. # score_result['suggestions']
  1016. # )
  1017. # results.append({
  1018. # 'sku': sku,
  1019. # 'final_score': score_result['final_score'],
  1020. # 'max_score': score_result['max_score'],
  1021. # 'breakdown': score_result['breakdown'],
  1022. # 'categorized_feedback': categorized, # NEW: Categorized by component
  1023. # 'ai_suggestions': score_result.get('ai_suggestions', {}),
  1024. # 'processing_time': score_result.get('processing_time', 0),
  1025. # # Keep original format for backward compatibility
  1026. # 'issues': score_result['issues'],
  1027. # 'suggestions': score_result['suggestions']
  1028. # })
  1029. # except Exception as e:
  1030. # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True)
  1031. # errors.append({'sku': product_data.get('sku'), 'error': str(e)})
  1032. # return JsonResponse({
  1033. # 'success': True,
  1034. # 'processed': len(results),
  1035. # 'results': results,
  1036. # 'errors': errors
  1037. # })
  1038. # except Exception as e:
  1039. # logger.error(f"Batch scoring error: {str(e)}", exc_info=True)
  1040. # return JsonResponse({'error': str(e)}, status=500)
  1041. @method_decorator(csrf_exempt, name='dispatch')
  1042. class BatchScoreView(View):
  1043. """Batch scoring with AI suggestions, content rules, and image scoring"""
  1044. def __init__(self, *args, **kwargs):
  1045. super().__init__(*args, **kwargs)
  1046. self.attribute_scorer = AttributeQualityScorer(use_ai=True)
  1047. self.image_scorer = ImageQualityScorer(use_ai=True)
  1048. def post(self, request):
  1049. try:
  1050. data = json.loads(request.body)
  1051. products = data.get('products', [])
  1052. if not products:
  1053. return JsonResponse({'error': 'No products provided'}, status=400)
  1054. results = []
  1055. errors = []
  1056. for product_data in products[:100]: # limit 100
  1057. sku = product_data.get('sku')
  1058. category = product_data.get('category')
  1059. if not sku or not category:
  1060. errors.append({'sku': sku, 'error': 'Missing SKU or category'})
  1061. continue
  1062. try:
  1063. # Get attribute rules
  1064. category_rules = list(CategoryAttributeRule.objects.filter(category=category).values())
  1065. if not category_rules:
  1066. errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'})
  1067. continue
  1068. # Get content rules
  1069. content_rules = list(
  1070. ProductContentRule.objects.filter(
  1071. Q(category__isnull=True) | Q(category=category)
  1072. ).values()
  1073. )
  1074. # Score attributes & content
  1075. score_result = self.attribute_scorer.score_product(
  1076. product_data,
  1077. category_rules,
  1078. content_rules=content_rules,
  1079. generate_ai_suggestions=True
  1080. )
  1081. import base64
  1082. with open("media/images/CLTH-001.jpg", "rb") as img:
  1083. img_base64 = base64.b64encode(img.read()).decode('utf-8')
  1084. # Score image if present
  1085. # image_data = product_data.get('image_bytes') # assume base64 or bytes
  1086. image_data = img_base64
  1087. # image_path = product_data.get('image_path') # optional path
  1088. import os
  1089. from django.conf import settings
  1090. # Get image path from payload
  1091. image_rel_path = product_data.get('image_path') # e.g., "images/CLTH-001.jpg"
  1092. print(f"Got image path from payload: {image_rel_path}")
  1093. image_full_path = None
  1094. if image_rel_path:
  1095. # Normalize path safely (remove MEDIA_URL if present)
  1096. cleaned_path = image_rel_path.replace(settings.MEDIA_URL, "").lstrip("/\\")
  1097. # image_full_path = os.path.join(settings.MEDIA_ROOT, cleaned_path)
  1098. image_full_path = "media/images/CLTH-001.jpg"
  1099. if not os.path.exists(image_full_path):
  1100. logger.warning(f"Image not found at {image_full_path} for SKU {sku}")
  1101. image_full_path = None
  1102. print(f"Resolved full image path: {image_full_path}")
  1103. # Set final path variable for image scorer (even if None)
  1104. image_path = image_full_path
  1105. image_score_result = None
  1106. try:
  1107. image_score_result = self.image_scorer.score_image(
  1108. product_data,
  1109. image_data=image_data,
  1110. image_path=image_path
  1111. )
  1112. except Exception as img_err:
  1113. logger.warning(f"Image scoring failed for SKU {sku}: {img_err}")
  1114. image_score_result = {
  1115. 'image_score': None,
  1116. 'breakdown': {},
  1117. 'issues': ['Image scoring failed'],
  1118. 'suggestions': [],
  1119. 'image_metadata': {},
  1120. 'ai_improvements': None
  1121. }
  1122. # Categorize issues and suggestions
  1123. categorized = categorize_issues_and_suggestions(
  1124. score_result['issues'] + image_score_result.get('issues', []),
  1125. score_result['suggestions'] + image_score_result.get('suggestions', [])
  1126. )
  1127. results.append({
  1128. 'sku': sku,
  1129. 'final_score': score_result['final_score'],
  1130. 'max_score': score_result['max_score'],
  1131. 'breakdown': score_result['breakdown'],
  1132. 'image_score': image_score_result.get('image_score'),
  1133. 'image_breakdown': image_score_result.get('breakdown', {}),
  1134. 'image_metadata': image_score_result.get('image_metadata', {}),
  1135. 'ai_suggestions': {
  1136. 'content': score_result.get('ai_suggestions', {}),
  1137. 'image': image_score_result.get('ai_improvements', {})
  1138. },
  1139. 'categorized_feedback': categorized,
  1140. 'processing_time': score_result.get('processing_time', 0),
  1141. # Original arrays for backward compatibility
  1142. 'issues': score_result['issues'] + image_score_result.get('issues', []),
  1143. 'suggestions': score_result['suggestions'] + image_score_result.get('suggestions', [])
  1144. })
  1145. except Exception as e:
  1146. logger.error(f"Error scoring product {sku}: {str(e)}", exc_info=True)
  1147. errors.append({'sku': sku, 'error': str(e)})
  1148. return JsonResponse({
  1149. 'success': True,
  1150. 'processed': len(results),
  1151. 'results': results,
  1152. 'errors': errors
  1153. })
  1154. except Exception as e:
  1155. logger.error(f"Batch scoring error: {str(e)}", exc_info=True)
  1156. return JsonResponse({'error': str(e)}, status=500)
  1157. @method_decorator(csrf_exempt, name='dispatch')
  1158. class ContentRulesView(View):
  1159. """API to manage ProductContentRules"""
  1160. def get(self, request):
  1161. """Get all content rules, optionally filtered by category"""
  1162. try:
  1163. category = request.GET.get('category')
  1164. if category:
  1165. rules = ProductContentRule.objects.filter(
  1166. Q(category__isnull=True) | Q(category=category)
  1167. )
  1168. else:
  1169. rules = ProductContentRule.objects.all()
  1170. rules_data = list(rules.values())
  1171. return JsonResponse({
  1172. 'success': True,
  1173. 'count': len(rules_data),
  1174. 'rules': rules_data
  1175. })
  1176. except Exception as e:
  1177. logger.error(f"Error fetching content rules: {e}", exc_info=True)
  1178. return JsonResponse({'error': str(e)}, status=500)
  1179. def post(self, request):
  1180. """Create a new content rule"""
  1181. try:
  1182. data = json.loads(request.body)
  1183. required_fields = ['field_name']
  1184. if not all(field in data for field in required_fields):
  1185. return JsonResponse({'error': 'field_name is required'}, status=400)
  1186. # Create rule
  1187. rule = ProductContentRule.objects.create(
  1188. category=data.get('category'),
  1189. field_name=data['field_name'],
  1190. is_mandatory=data.get('is_mandatory', True),
  1191. min_length=data.get('min_length'),
  1192. max_length=data.get('max_length'),
  1193. min_word_count=data.get('min_word_count'),
  1194. max_word_count=data.get('max_word_count'),
  1195. must_contain_keywords=data.get('must_contain_keywords', []),
  1196. validation_regex=data.get('validation_regex', ''),
  1197. description=data.get('description', '')
  1198. )
  1199. # Clear cache
  1200. if data.get('category'):
  1201. cache.delete(f"content_rules_{data['category']}")
  1202. return JsonResponse({
  1203. 'success': True,
  1204. 'rule_id': rule.id,
  1205. 'message': 'Content rule created successfully'
  1206. })
  1207. except Exception as e:
  1208. logger.error(f"Error creating content rule: {e}", exc_info=True)
  1209. return JsonResponse({'error': str(e)}, status=500)
  1210. @method_decorator(csrf_exempt, name='dispatch')
  1211. class ProductScoreDetailView(View):
  1212. """Get detailed score for a specific product"""
  1213. def get(self, request, sku):
  1214. try:
  1215. product = get_object_or_404(Product, sku=sku)
  1216. # Get latest score
  1217. latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first()
  1218. if not latest_score:
  1219. return JsonResponse({'error': 'No score found for this product'}, status=404)
  1220. # Get interpretation
  1221. scorer = AttributeQualityScorer()
  1222. interpretation = scorer.get_score_interpretation(latest_score.score)
  1223. # Categorize issues and suggestions
  1224. categorized = categorize_issues_and_suggestions(
  1225. latest_score.issues,
  1226. latest_score.suggestions
  1227. )
  1228. return JsonResponse({
  1229. 'success': True,
  1230. 'product': {
  1231. 'sku': product.sku,
  1232. 'category': product.category,
  1233. 'title': product.title,
  1234. 'description': product.description,
  1235. 'short_description': product.short_description,
  1236. 'seo_title': product.seo_title,
  1237. 'seo_description': product.seo_description,
  1238. 'attributes': product.attributes
  1239. },
  1240. 'score': {
  1241. 'final_score': latest_score.score,
  1242. 'max_score': latest_score.max_score,
  1243. 'breakdown': latest_score.details,
  1244. 'interpretation': interpretation
  1245. },
  1246. 'categorized_feedback': categorized, # NEW: Categorized by component
  1247. 'ai_suggestions': latest_score.ai_suggestions,
  1248. 'scored_at': latest_score.created_at.isoformat(),
  1249. # Keep original format for backward compatibility
  1250. 'issues': latest_score.issues,
  1251. 'suggestions': latest_score.suggestions
  1252. })
  1253. except Exception as e:
  1254. logger.error(f"Error fetching product score: {e}", exc_info=True)
  1255. return JsonResponse({'error': str(e)}, status=500)