# # views.py (Enhanced) # from django.shortcuts import render, get_object_or_404 # from django.http import JsonResponse # from django.views import View # from django.core.cache import cache # import json # import logging # from core.models import AttributeScore, CategoryAttributeRule, Product # from core.services.attribute_scorer import AttributeQualityScorer # from django.views.decorators.csrf import csrf_exempt # from django.utils.decorators import method_decorator # logger = logging.getLogger(__name__) # @method_decorator(csrf_exempt, name='dispatch') # class AttributeScoreView(View): # """Enhanced API view with caching and AI suggestions""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # enable AI # def post(self, request, *args, **kwargs): # """Score a single product with AI suggestions""" # try: # data = json.loads(request.body) # product_data = data.get('product', {}) # sku = product_data.get('sku') # use_ai = data.get('use_ai', True) # if not sku: # return JsonResponse({'error': 'SKU is required'}, status=400) # category = product_data.get('category', '') # if not category: # return JsonResponse({'error': 'Category is required'}, status=400) # # Get or create product # product, created = Product.objects.get_or_create( # sku=sku, # defaults={ # 'title': product_data.get('title', ''), # 'description': product_data.get('description', ''), # 'category': category, # 'attributes': product_data.get('attributes', {}) # } # ) # # Update if exists # if not created: # product.title = product_data.get('title', product.title) # product.description = product_data.get('description', product.description) # product.attributes = product_data.get('attributes', product.attributes) # product.save() # # Get rules (cached) # cache_key = f"category_rules_{category}" # rules = cache.get(cache_key) # if rules is None: # rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # cache.set(cache_key, rules, 3600) # if not rules: # return JsonResponse({'error': f'No rules defined for {category}'}, status=400) # # Force AI suggestions # score_result = self.scorer.score_product( # { # 'sku': product.sku, # 'category': product.category, # 'title': product.title, # 'description': product.description, # 'attributes': product.attributes # }, # rules, # generate_ai_suggestions=True # always generate AI # ) # # Save score # AttributeScore.objects.create( # product=product, # score=score_result['final_score'], # max_score=score_result['max_score'], # details=score_result['breakdown'], # issues=score_result['issues'], # suggestions=score_result['suggestions'], # ai_suggestions=score_result.get('ai_suggestions', {}), # processing_time=score_result.get('processing_time', 0) # ) # return JsonResponse({ # 'success': True, # 'product_sku': sku, # 'created': created, # 'score_result': score_result # }) # except json.JSONDecodeError: # return JsonResponse({'error': 'Invalid JSON'}, status=400) # except Exception as e: # logger.error(f"Error scoring product: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # from django.views import View # from django.http import JsonResponse # from django.utils.decorators import method_decorator # from django.views.decorators.csrf import csrf_exempt # import json # import logging # from .models import Product, CategoryAttributeRule, AttributeScore # from .services.attribute_scorer import AttributeQualityScorer # logger = logging.getLogger(__name__) # @method_decorator(csrf_exempt, name='dispatch') # class BatchScoreView(View): # """Batch scoring with AI suggestions""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # enable AI even for batch # def post(self, request): # try: # data = json.loads(request.body) # products = data.get('products', []) # if not products: # return JsonResponse({'error': 'No products provided'}, status=400) # results = [] # errors = [] # for product_data in products[:100]: # limit 100 # try: # sku = product_data.get('sku') # category = product_data.get('category') # if not sku or not category: # errors.append({'sku': sku, 'error': 'Missing SKU or category'}) # continue # # Get rules # rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # if not rules: # errors.append({'sku': sku, 'error': f'No rules for category {category}'}) # continue # # Force AI suggestions # score_result = self.scorer.score_product( # product_data, # rules, # generate_ai_suggestions=True # <- key change # ) # results.append({ # 'sku': sku, # 'final_score': score_result['final_score'], # 'max_score': score_result['max_score'], # 'breakdown': score_result['breakdown'], # 'issues': score_result['issues'], # 'suggestions': score_result['suggestions'], # 'ai_suggestions': score_result.get('ai_suggestions', {}), # 'processing_time': score_result.get('processing_time', 0) # }) # except Exception as e: # errors.append({'sku': product_data.get('sku'), 'error': str(e)}) # return JsonResponse({ # 'success': True, # 'processed': len(results), # 'results': results, # 'errors': errors # }) # except Exception as e: # logger.error(f"Batch scoring error: {str(e)}") # return JsonResponse({'error': str(e)}, status=500) # # views.py (Enhanced with ProductContentRule support - FIXED) # from django.shortcuts import render, get_object_or_404 # from django.http import JsonResponse # from django.views import View # from django.core.cache import cache # from django.db.models import Q # ← FIXED: Import Q from django.db.models # from django.views.decorators.csrf import csrf_exempt # from django.utils.decorators import method_decorator # import json # import logging # from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product # from core.services.attribute_scorer import AttributeQualityScorer # logger = logging.getLogger(__name__) # @method_decorator(csrf_exempt, name='dispatch') # class AttributeScoreView(View): # """Enhanced API view with ProductContentRule support""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # def post(self, request, *args, **kwargs): # """Score a single product with AI suggestions and content rules validation""" # try: # data = json.loads(request.body) # product_data = data.get('product', {}) # sku = product_data.get('sku') # use_ai = data.get('use_ai', True) # if not sku: # return JsonResponse({'error': 'SKU is required'}, status=400) # category = product_data.get('category', '') # if not category: # return JsonResponse({'error': 'Category is required'}, status=400) # # Get or create product # product, created = Product.objects.get_or_create( # sku=sku, # defaults={ # 'title': product_data.get('title', ''), # 'description': product_data.get('description', ''), # 'short_description': product_data.get('short_description', ''), # 'seo_title': product_data.get('seo_title', ''), # 'seo_description': product_data.get('seo_description', ''), # 'category': category, # 'attributes': product_data.get('attributes', {}) # } # ) # # Update if exists # if not created: # product.title = product_data.get('title', product.title) # product.description = product_data.get('description', product.description) # product.short_description = product_data.get('short_description', product.short_description) # product.seo_title = product_data.get('seo_title', product.seo_title) # product.seo_description = product_data.get('seo_description', product.seo_description) # product.attributes = product_data.get('attributes', product.attributes) # product.save() # # Get CategoryAttributeRules (cached) # cache_key = f"category_rules_{category}" # category_rules = cache.get(cache_key) # if category_rules is None: # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # cache.set(cache_key, category_rules, 3600) # if not category_rules: # return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400) # # Get ProductContentRules (cached) - FIXED: Use Q from django.db.models # content_cache_key = f"content_rules_{category}" # content_rules = cache.get(content_cache_key) # if content_rules is None: # # Get both global rules (category=None) and category-specific rules # content_rules = list( # ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ).values() # ) # cache.set(content_cache_key, content_rules, 3600) # # Build product dict with all fields # product_dict = { # 'sku': product.sku, # 'category': product.category, # 'title': product.title, # 'description': product.description, # 'short_description': product.short_description, # 'seo_title': product.seo_title, # 'seo_description': product.seo_description, # 'attributes': product.attributes # } # # Score product with content rules # score_result = self.scorer.score_product( # product_dict, # category_rules, # content_rules=content_rules, # generate_ai_suggestions=True # ) # # Save score # AttributeScore.objects.create( # product=product, # score=score_result['final_score'], # max_score=score_result['max_score'], # details=score_result['breakdown'], # issues=score_result['issues'], # suggestions=score_result['suggestions'], # ai_suggestions=score_result.get('ai_suggestions', {}), # processing_time=score_result.get('processing_time', 0) # ) # return JsonResponse({ # 'success': True, # 'product_sku': sku, # 'created': created, # 'score_result': score_result # }) # except json.JSONDecodeError: # return JsonResponse({'error': 'Invalid JSON'}, status=400) # except Exception as e: # logger.error(f"Error scoring product: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class BatchScoreView(View): # """Batch scoring with AI suggestions and content rules""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # def post(self, request): # try: # data = json.loads(request.body) # products = data.get('products', []) # if not products: # return JsonResponse({'error': 'No products provided'}, status=400) # results = [] # errors = [] # for product_data in products[:100]: # limit 100 # try: # sku = product_data.get('sku') # category = product_data.get('category') # if not sku or not category: # errors.append({'sku': sku, 'error': 'Missing SKU or category'}) # continue # # Get attribute rules # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # if not category_rules: # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'}) # continue # # Get content rules - FIXED: Use Q from django.db.models # content_rules = list( # ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ).values() # ) # # Score with content rules # score_result = self.scorer.score_product( # product_data, # category_rules, # content_rules=content_rules, # generate_ai_suggestions=True # ) # results.append({ # 'sku': sku, # 'final_score': score_result['final_score'], # 'max_score': score_result['max_score'], # 'breakdown': score_result['breakdown'], # 'issues': score_result['issues'], # 'suggestions': score_result['suggestions'], # 'ai_suggestions': score_result.get('ai_suggestions', {}), # 'processing_time': score_result.get('processing_time', 0) # }) # except Exception as e: # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True) # errors.append({'sku': product_data.get('sku'), 'error': str(e)}) # return JsonResponse({ # 'success': True, # 'processed': len(results), # 'results': results, # 'errors': errors # }) # except Exception as e: # logger.error(f"Batch scoring error: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class ContentRulesView(View): # """API to manage ProductContentRules""" # def get(self, request): # """Get all content rules, optionally filtered by category""" # try: # category = request.GET.get('category') # if category: # # FIXED: Use Q from django.db.models # rules = ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ) # else: # rules = ProductContentRule.objects.all() # rules_data = list(rules.values()) # return JsonResponse({ # 'success': True, # 'count': len(rules_data), # 'rules': rules_data # }) # except Exception as e: # logger.error(f"Error fetching content rules: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # def post(self, request): # """Create a new content rule""" # try: # data = json.loads(request.body) # required_fields = ['field_name'] # if not all(field in data for field in required_fields): # return JsonResponse({'error': 'field_name is required'}, status=400) # # Create rule # rule = ProductContentRule.objects.create( # category=data.get('category'), # field_name=data['field_name'], # is_mandatory=data.get('is_mandatory', True), # min_length=data.get('min_length'), # max_length=data.get('max_length'), # min_word_count=data.get('min_word_count'), # max_word_count=data.get('max_word_count'), # must_contain_keywords=data.get('must_contain_keywords', []), # validation_regex=data.get('validation_regex', ''), # description=data.get('description', '') # ) # # Clear cache # if data.get('category'): # cache.delete(f"content_rules_{data['category']}") # return JsonResponse({ # 'success': True, # 'rule_id': rule.id, # 'message': 'Content rule created successfully' # }) # except Exception as e: # logger.error(f"Error creating content rule: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class ProductScoreDetailView(View): # """Get detailed score for a specific product""" # def get(self, request, sku): # try: # product = get_object_or_404(Product, sku=sku) # # Get latest score # latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first() # if not latest_score: # return JsonResponse({'error': 'No score found for this product'}, status=404) # # Get interpretation # scorer = AttributeQualityScorer() # interpretation = scorer.get_score_interpretation(latest_score.score) # return JsonResponse({ # 'success': True, # 'product': { # 'sku': product.sku, # 'category': product.category, # 'title': product.title, # 'description': product.description, # 'short_description': product.short_description, # 'seo_title': product.seo_title, # 'seo_description': product.seo_description, # 'attributes': product.attributes # }, # 'score': { # 'final_score': latest_score.score, # 'max_score': latest_score.max_score, # 'breakdown': latest_score.details, # 'interpretation': interpretation # }, # 'issues': latest_score.issues, # 'suggestions': latest_score.suggestions, # 'ai_suggestions': latest_score.ai_suggestions, # 'scored_at': latest_score.created_at.isoformat() # }) # except Exception as e: # logger.error(f"Error fetching product score: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # # views.py (Enhanced with categorized issues and suggestions) # from django.shortcuts import render, get_object_or_404 # from django.http import JsonResponse # from django.views import View # from django.core.cache import cache # from django.db.models import Q # from django.views.decorators.csrf import csrf_exempt # from django.utils.decorators import method_decorator # import json # import logging # from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product # from core.services.attribute_scorer import AttributeQualityScorer # logger = logging.getLogger(__name__) # def categorize_issues_and_suggestions(issues, suggestions): # """ # Categorize issues and suggestions by component (attributes, title, description, seo) # Returns a structured dict with categorized items # """ # categorized = { # 'attributes': {'issues': [], 'suggestions': []}, # 'title': {'issues': [], 'suggestions': []}, # 'description': {'issues': [], 'suggestions': []}, # 'seo': {'issues': [], 'suggestions': []}, # 'content_rules': {'issues': [], 'suggestions': []}, # 'general': {'issues': [], 'suggestions': []} # } # # Categorize issues # for issue in issues: # issue_lower = issue.lower() # if issue.startswith('Title:'): # categorized['title']['issues'].append(issue.replace('Title:', '').strip()) # elif issue.startswith('Description:'): # categorized['description']['issues'].append(issue.replace('Description:', '').strip()) # elif issue.startswith('SEO:'): # categorized['seo']['issues'].append(issue.replace('SEO:', '').strip()) # elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']): # categorized['seo']['issues'].append(issue) # elif 'content rule' in issue_lower or any(field in issue for field in ['Seo Title:', 'Seo Description:']): # categorized['content_rules']['issues'].append(issue) # elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']): # categorized['attributes']['issues'].append(issue) # else: # categorized['general']['issues'].append(issue) # # Categorize suggestions # for suggestion in suggestions: # suggestion_lower = suggestion.lower() # # Check for explicit component prefixes # if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']): # categorized['title']['suggestions'].append(suggestion) # elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']): # categorized['description']['suggestions'].append(suggestion) # elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']): # categorized['seo']['suggestions'].append(suggestion) # elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description', 'add seo']): # categorized['seo']['suggestions'].append(suggestion) # elif 'content rule' in suggestion_lower: # categorized['content_rules']['suggestions'].append(suggestion) # elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize', 'correct capitalization']): # categorized['attributes']['suggestions'].append(suggestion) # else: # categorized['general']['suggestions'].append(suggestion) # # Remove empty categories # return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']} # @method_decorator(csrf_exempt, name='dispatch') # class AttributeScoreView(View): # """Enhanced API view with ProductContentRule support""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # def post(self, request, *args, **kwargs): # """Score a single product with AI suggestions and content rules validation""" # try: # data = json.loads(request.body) # product_data = data.get('product', {}) # sku = product_data.get('sku') # use_ai = data.get('use_ai', True) # if not sku: # return JsonResponse({'error': 'SKU is required'}, status=400) # category = product_data.get('category', '') # if not category: # return JsonResponse({'error': 'Category is required'}, status=400) # # Get or create product # product, created = Product.objects.get_or_create( # sku=sku, # defaults={ # 'title': product_data.get('title', ''), # 'description': product_data.get('description', ''), # 'short_description': product_data.get('short_description', ''), # 'seo_title': product_data.get('seo_title', ''), # 'seo_description': product_data.get('seo_description', ''), # 'category': category, # 'attributes': product_data.get('attributes', {}) # } # ) # # Update if exists # if not created: # product.title = product_data.get('title', product.title) # product.description = product_data.get('description', product.description) # product.short_description = product_data.get('short_description', product.short_description) # product.seo_title = product_data.get('seo_title', product.seo_title) # product.seo_description = product_data.get('seo_description', product.seo_description) # product.attributes = product_data.get('attributes', product.attributes) # product.save() # # Get CategoryAttributeRules (cached) # cache_key = f"category_rules_{category}" # category_rules = cache.get(cache_key) # if category_rules is None: # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # cache.set(cache_key, category_rules, 3600) # if not category_rules: # return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400) # # Get ProductContentRules (cached) # content_cache_key = f"content_rules_{category}" # content_rules = cache.get(content_cache_key) # if content_rules is None: # content_rules = list( # ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ).values() # ) # cache.set(content_cache_key, content_rules, 3600) # # Build product dict with all fields # product_dict = { # 'sku': product.sku, # 'category': product.category, # 'title': product.title, # 'description': product.description, # 'short_description': product.short_description, # 'seo_title': product.seo_title, # 'seo_description': product.seo_description, # 'attributes': product.attributes # } # # Score product with content rules # score_result = self.scorer.score_product( # product_dict, # category_rules, # content_rules=content_rules, # generate_ai_suggestions=True # ) # # Categorize issues and suggestions # categorized = categorize_issues_and_suggestions( # score_result['issues'], # score_result['suggestions'] # ) # # Save score # AttributeScore.objects.create( # product=product, # score=score_result['final_score'], # max_score=score_result['max_score'], # details=score_result['breakdown'], # issues=score_result['issues'], # suggestions=score_result['suggestions'], # ai_suggestions=score_result.get('ai_suggestions', {}), # processing_time=score_result.get('processing_time', 0) # ) # # Enhanced response with categorized issues # return JsonResponse({ # 'success': True, # 'product_sku': sku, # 'created': created, # 'score_result': { # 'final_score': score_result['final_score'], # 'max_score': score_result['max_score'], # 'breakdown': score_result['breakdown'], # 'categorized_feedback': categorized, # NEW: Categorized issues/suggestions # 'ai_suggestions': score_result.get('ai_suggestions', {}), # 'processing_time': score_result.get('processing_time', 0), # # Keep original format for backward compatibility # 'issues': score_result['issues'], # 'suggestions': score_result['suggestions'] # } # }) # except json.JSONDecodeError: # return JsonResponse({'error': 'Invalid JSON'}, status=400) # except Exception as e: # logger.error(f"Error scoring product: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class BatchScoreView(View): # """Batch scoring with AI suggestions and content rules""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # def post(self, request): # try: # data = json.loads(request.body) # products = data.get('products', []) # if not products: # return JsonResponse({'error': 'No products provided'}, status=400) # results = [] # errors = [] # for product_data in products[:100]: # limit 100 # try: # sku = product_data.get('sku') # category = product_data.get('category') # if not sku or not category: # errors.append({'sku': sku, 'error': 'Missing SKU or category'}) # continue # # Get attribute rules # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # if not category_rules: # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'}) # continue # # Get content rules # content_rules = list( # ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ).values() # ) # # Score with content rules # score_result = self.scorer.score_product( # product_data, # category_rules, # content_rules=content_rules, # generate_ai_suggestions=True # ) # # Categorize issues and suggestions # categorized = categorize_issues_and_suggestions( # score_result['issues'], # score_result['suggestions'] # ) # results.append({ # 'sku': sku, # 'final_score': score_result['final_score'], # 'max_score': score_result['max_score'], # 'breakdown': score_result['breakdown'], # 'categorized_feedback': categorized, # NEW: Categorized by component # 'ai_suggestions': score_result.get('ai_suggestions', {}), # 'processing_time': score_result.get('processing_time', 0), # # Keep original format for backward compatibility # 'issues': score_result['issues'], # 'suggestions': score_result['suggestions'] # }) # except Exception as e: # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True) # errors.append({'sku': product_data.get('sku'), 'error': str(e)}) # return JsonResponse({ # 'success': True, # 'processed': len(results), # 'results': results, # 'errors': errors # }) # except Exception as e: # logger.error(f"Batch scoring error: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class ContentRulesView(View): # """API to manage ProductContentRules""" # def get(self, request): # """Get all content rules, optionally filtered by category""" # try: # category = request.GET.get('category') # if category: # rules = ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ) # else: # rules = ProductContentRule.objects.all() # rules_data = list(rules.values()) # return JsonResponse({ # 'success': True, # 'count': len(rules_data), # 'rules': rules_data # }) # except Exception as e: # logger.error(f"Error fetching content rules: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # def post(self, request): # """Create a new content rule""" # try: # data = json.loads(request.body) # required_fields = ['field_name'] # if not all(field in data for field in required_fields): # return JsonResponse({'error': 'field_name is required'}, status=400) # # Create rule # rule = ProductContentRule.objects.create( # category=data.get('category'), # field_name=data['field_name'], # is_mandatory=data.get('is_mandatory', True), # min_length=data.get('min_length'), # max_length=data.get('max_length'), # min_word_count=data.get('min_word_count'), # max_word_count=data.get('max_word_count'), # must_contain_keywords=data.get('must_contain_keywords', []), # validation_regex=data.get('validation_regex', ''), # description=data.get('description', '') # ) # # Clear cache # if data.get('category'): # cache.delete(f"content_rules_{data['category']}") # return JsonResponse({ # 'success': True, # 'rule_id': rule.id, # 'message': 'Content rule created successfully' # }) # except Exception as e: # logger.error(f"Error creating content rule: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class ProductScoreDetailView(View): # """Get detailed score for a specific product""" # def get(self, request, sku): # try: # product = get_object_or_404(Product, sku=sku) # # Get latest score # latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first() # if not latest_score: # return JsonResponse({'error': 'No score found for this product'}, status=404) # # Get interpretation # scorer = AttributeQualityScorer() # interpretation = scorer.get_score_interpretation(latest_score.score) # # Categorize issues and suggestions # categorized = categorize_issues_and_suggestions( # latest_score.issues, # latest_score.suggestions # ) # return JsonResponse({ # 'success': True, # 'product': { # 'sku': product.sku, # 'category': product.category, # 'title': product.title, # 'description': product.description, # 'short_description': product.short_description, # 'seo_title': product.seo_title, # 'seo_description': product.seo_description, # 'attributes': product.attributes # }, # 'score': { # 'final_score': latest_score.score, # 'max_score': latest_score.max_score, # 'breakdown': latest_score.details, # 'interpretation': interpretation # }, # 'categorized_feedback': categorized, # NEW: Categorized by component # 'ai_suggestions': latest_score.ai_suggestions, # 'scored_at': latest_score.created_at.isoformat(), # # Keep original format for backward compatibility # 'issues': latest_score.issues, # 'suggestions': latest_score.suggestions # }) # except Exception as e: # logger.error(f"Error fetching product score: {e}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) # views.py (Enhanced with categorized issues and suggestions) from django.shortcuts import render, get_object_or_404 from django.http import JsonResponse from django.views import View from django.core.cache import cache from django.db.models import Q from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator import json import logging from core.services.image_scorer import ImageQualityScorer from core.models import AttributeScore, CategoryAttributeRule, ProductContentRule, Product from core.services.attribute_scorer import AttributeQualityScorer logger = logging.getLogger(__name__) def categorize_ai_improvements(ai_suggestions): """ Consolidate AI improvements by component Returns a structured dict with grouped improvements per component """ if not ai_suggestions or 'improvements' not in ai_suggestions: return None grouped = { 'attributes': [], 'title': [], 'description': [], 'seo': [], 'general': [] } for improvement in ai_suggestions.get('improvements', []): component = improvement.get('component', 'general').lower() # Normalize component names if component in grouped: grouped[component].append({ 'issue': improvement.get('issue', ''), 'suggestion': improvement.get('suggestion', ''), 'priority': improvement.get('priority', 'medium'), 'confidence': improvement.get('confidence', 'medium') }) else: grouped['general'].append({ 'issue': improvement.get('issue', ''), 'suggestion': improvement.get('suggestion', ''), 'priority': improvement.get('priority', 'medium'), 'confidence': improvement.get('confidence', 'medium') }) # Remove empty categories grouped = {k: v for k, v in grouped.items() if v} return grouped def categorize_issues_and_suggestions(issues, suggestions): """ Categorize issues and suggestions by component (attributes, title, description, seo) Returns a structured dict with categorized items """ categorized = { 'attributes': {'issues': [], 'suggestions': []}, 'title': {'issues': [], 'suggestions': []}, 'description': {'issues': [], 'suggestions': []}, 'seo': {'issues': [], 'suggestions': []}, 'content_rules': {'issues': [], 'suggestions': []}, 'general': {'issues': [], 'suggestions': []} } # Categorize issues for issue in issues: issue_lower = issue.lower() if issue.startswith('Title:'): categorized['title']['issues'].append(issue.replace('Title:', '').strip()) elif issue.startswith('Description:'): categorized['description']['issues'].append(issue.replace('Description:', '').strip()) elif issue.startswith('SEO:'): categorized['seo']['issues'].append(issue.replace('SEO:', '').strip()) elif any(field in issue_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description']): categorized['seo']['issues'].append(issue) elif 'content rule' in issue_lower or any(field in issue for field in ['Seo Title:', 'Seo Description:']): categorized['content_rules']['issues'].append(issue) elif any(keyword in issue_lower for keyword in ['mandatory field', 'attribute', 'valid values', 'placeholder', 'standardiz']): categorized['attributes']['issues'].append(issue) else: categorized['general']['issues'].append(issue) # Categorize suggestions for suggestion in suggestions: suggestion_lower = suggestion.lower() # Check for explicit component prefixes if any(prefix in suggestion_lower for prefix in ['title:', 'expand title', 'shorten title', 'add brand name']): categorized['title']['suggestions'].append(suggestion) elif any(prefix in suggestion_lower for prefix in ['description:', 'expand description', 'write comprehensive', 'add features']): categorized['description']['suggestions'].append(suggestion) elif any(prefix in suggestion_lower for prefix in ['seo:', 'improve seo', 'add keywords', 'search', 'discoverability']): categorized['seo']['suggestions'].append(suggestion) elif any(field in suggestion_lower for field in ['seo_title', 'seo title', 'seo_description', 'seo description', 'add seo']): categorized['seo']['suggestions'].append(suggestion) elif 'content rule' in suggestion_lower: categorized['content_rules']['suggestions'].append(suggestion) elif any(keyword in suggestion_lower for keyword in ['add required', 'add mandatory', 'provide a', 'attribute', 'standardize', 'correct capitalization']): categorized['attributes']['suggestions'].append(suggestion) else: categorized['general']['suggestions'].append(suggestion) # Remove empty categories return {k: v for k, v in categorized.items() if v['issues'] or v['suggestions']} @method_decorator(csrf_exempt, name='dispatch') class AttributeScoreView(View): """Enhanced API view with ProductContentRule support""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.scorer = AttributeQualityScorer(use_ai=True) def post(self, request, *args, **kwargs): """Score a single product with AI suggestions and content rules validation""" try: data = json.loads(request.body) product_data = data.get('product', {}) sku = product_data.get('sku') use_ai = data.get('use_ai', True) if not sku: return JsonResponse({'error': 'SKU is required'}, status=400) category = product_data.get('category', '') if not category: return JsonResponse({'error': 'Category is required'}, status=400) # Get or create product product, created = Product.objects.get_or_create( sku=sku, defaults={ 'title': product_data.get('title', ''), 'description': product_data.get('description', ''), 'short_description': product_data.get('short_description', ''), 'seo_title': product_data.get('seo_title', ''), 'seo_description': product_data.get('seo_description', ''), 'category': category, 'attributes': product_data.get('attributes', {}) } ) # Update if exists if not created: product.title = product_data.get('title', product.title) product.description = product_data.get('description', product.description) product.short_description = product_data.get('short_description', product.short_description) product.seo_title = product_data.get('seo_title', product.seo_title) product.seo_description = product_data.get('seo_description', product.seo_description) product.attributes = product_data.get('attributes', product.attributes) product.save() # Get CategoryAttributeRules (cached) cache_key = f"category_rules_{category}" category_rules = cache.get(cache_key) if category_rules is None: category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) cache.set(cache_key, category_rules, 3600) if not category_rules: return JsonResponse({'error': f'No attribute rules defined for {category}'}, status=400) # Get ProductContentRules (cached) content_cache_key = f"content_rules_{category}" content_rules = cache.get(content_cache_key) if content_rules is None: content_rules = list( ProductContentRule.objects.filter( Q(category__isnull=True) | Q(category=category) ).values() ) cache.set(content_cache_key, content_rules, 3600) # Build product dict with all fields product_dict = { 'sku': product.sku, 'category': product.category, 'title': product.title, 'description': product.description, 'short_description': product.short_description, 'seo_title': product.seo_title, 'seo_description': product.seo_description, 'attributes': product.attributes } # Score product with content rules score_result = self.scorer.score_product( product_dict, category_rules, content_rules=content_rules, generate_ai_suggestions=True ) # Categorize issues and suggestions categorized = categorize_issues_and_suggestions( score_result['issues'], score_result['suggestions'] ) # Save score AttributeScore.objects.create( product=product, score=score_result['final_score'], max_score=score_result['max_score'], details=score_result['breakdown'], issues=score_result['issues'], suggestions=score_result['suggestions'], ai_suggestions=score_result.get('ai_suggestions', {}), processing_time=score_result.get('processing_time', 0) ) # Enhanced response with categorized issues return JsonResponse({ 'success': True, 'product_sku': sku, 'created': created, 'score_result': { 'final_score': score_result['final_score'], 'max_score': score_result['max_score'], 'breakdown': score_result['breakdown'], 'categorized_feedback': categorized, # NEW: Categorized issues/suggestions 'ai_suggestions': score_result.get('ai_suggestions', {}), 'processing_time': score_result.get('processing_time', 0), # Keep original format for backward compatibility 'issues': score_result['issues'], 'suggestions': score_result['suggestions'] } }) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) except Exception as e: logger.error(f"Error scoring product: {str(e)}", exc_info=True) return JsonResponse({'error': str(e)}, status=500) # @method_decorator(csrf_exempt, name='dispatch') # class BatchScoreView(View): # """Batch scoring with AI suggestions and content rules""" # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) # self.scorer = AttributeQualityScorer(use_ai=True) # def post(self, request): # try: # data = json.loads(request.body) # products = data.get('products', []) # if not products: # return JsonResponse({'error': 'No products provided'}, status=400) # results = [] # errors = [] # for product_data in products[:100]: # limit 100 # try: # sku = product_data.get('sku') # category = product_data.get('category') # if not sku or not category: # errors.append({'sku': sku, 'error': 'Missing SKU or category'}) # continue # # Get attribute rules # category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) # if not category_rules: # errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'}) # continue # # Get content rules # content_rules = list( # ProductContentRule.objects.filter( # Q(category__isnull=True) | Q(category=category) # ).values() # ) # # Score with content rules # score_result = self.scorer.score_product( # product_data, # category_rules, # content_rules=content_rules, # generate_ai_suggestions=True # ) # # Categorize issues and suggestions # categorized = categorize_issues_and_suggestions( # score_result['issues'], # score_result['suggestions'] # ) # results.append({ # 'sku': sku, # 'final_score': score_result['final_score'], # 'max_score': score_result['max_score'], # 'breakdown': score_result['breakdown'], # 'categorized_feedback': categorized, # NEW: Categorized by component # 'ai_suggestions': score_result.get('ai_suggestions', {}), # 'processing_time': score_result.get('processing_time', 0), # # Keep original format for backward compatibility # 'issues': score_result['issues'], # 'suggestions': score_result['suggestions'] # }) # except Exception as e: # logger.error(f"Error scoring product {product_data.get('sku')}: {str(e)}", exc_info=True) # errors.append({'sku': product_data.get('sku'), 'error': str(e)}) # return JsonResponse({ # 'success': True, # 'processed': len(results), # 'results': results, # 'errors': errors # }) # except Exception as e: # logger.error(f"Batch scoring error: {str(e)}", exc_info=True) # return JsonResponse({'error': str(e)}, status=500) @method_decorator(csrf_exempt, name='dispatch') class BatchScoreView(View): """Batch scoring with AI suggestions, content rules, and image scoring""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attribute_scorer = AttributeQualityScorer(use_ai=True) self.image_scorer = ImageQualityScorer(use_ai=True) def post(self, request): try: data = json.loads(request.body) products = data.get('products', []) if not products: return JsonResponse({'error': 'No products provided'}, status=400) results = [] errors = [] for product_data in products[:100]: # limit 100 sku = product_data.get('sku') category = product_data.get('category') if not sku or not category: errors.append({'sku': sku, 'error': 'Missing SKU or category'}) continue try: # Get attribute rules category_rules = list(CategoryAttributeRule.objects.filter(category=category).values()) if not category_rules: errors.append({'sku': sku, 'error': f'No attribute rules for category {category}'}) continue # Get content rules content_rules = list( ProductContentRule.objects.filter( Q(category__isnull=True) | Q(category=category) ).values() ) # Score attributes & content score_result = self.attribute_scorer.score_product( product_data, category_rules, content_rules=content_rules, generate_ai_suggestions=True ) import base64 with open("media/images/CLTH-001.jpg", "rb") as img: img_base64 = base64.b64encode(img.read()).decode('utf-8') # Score image if present # image_data = product_data.get('image_bytes') # assume base64 or bytes image_data = img_base64 # image_path = product_data.get('image_path') # optional path import os from django.conf import settings # Get image path from payload image_rel_path = product_data.get('image_path') # e.g., "images/CLTH-001.jpg" print(f"Got image path from payload: {image_rel_path}") image_full_path = None if image_rel_path: # Normalize path safely (remove MEDIA_URL if present) cleaned_path = image_rel_path.replace(settings.MEDIA_URL, "").lstrip("/\\") # image_full_path = os.path.join(settings.MEDIA_ROOT, cleaned_path) image_full_path = "media/images/CLTH-001.jpg" if not os.path.exists(image_full_path): logger.warning(f"Image not found at {image_full_path} for SKU {sku}") image_full_path = None print(f"Resolved full image path: {image_full_path}") # Set final path variable for image scorer (even if None) image_path = image_full_path image_score_result = None try: image_score_result = self.image_scorer.score_image( product_data, image_data=image_data, image_path=image_path ) except Exception as img_err: logger.warning(f"Image scoring failed for SKU {sku}: {img_err}") image_score_result = { 'image_score': None, 'breakdown': {}, 'issues': ['Image scoring failed'], 'suggestions': [], 'image_metadata': {}, 'ai_improvements': None } # Categorize issues and suggestions categorized = categorize_issues_and_suggestions( score_result['issues'] + image_score_result.get('issues', []), score_result['suggestions'] + image_score_result.get('suggestions', []) ) results.append({ 'sku': sku, 'final_score': score_result['final_score'], 'max_score': score_result['max_score'], 'breakdown': score_result['breakdown'], 'image_score': image_score_result.get('image_score'), 'image_breakdown': image_score_result.get('breakdown', {}), 'image_metadata': image_score_result.get('image_metadata', {}), 'ai_suggestions': { 'content': score_result.get('ai_suggestions', {}), 'image': image_score_result.get('ai_improvements', {}) }, 'categorized_feedback': categorized, 'processing_time': score_result.get('processing_time', 0), # Original arrays for backward compatibility 'issues': score_result['issues'] + image_score_result.get('issues', []), 'suggestions': score_result['suggestions'] + image_score_result.get('suggestions', []) }) except Exception as e: logger.error(f"Error scoring product {sku}: {str(e)}", exc_info=True) errors.append({'sku': sku, 'error': str(e)}) return JsonResponse({ 'success': True, 'processed': len(results), 'results': results, 'errors': errors }) except Exception as e: logger.error(f"Batch scoring error: {str(e)}", exc_info=True) return JsonResponse({'error': str(e)}, status=500) @method_decorator(csrf_exempt, name='dispatch') class ContentRulesView(View): """API to manage ProductContentRules""" def get(self, request): """Get all content rules, optionally filtered by category""" try: category = request.GET.get('category') if category: rules = ProductContentRule.objects.filter( Q(category__isnull=True) | Q(category=category) ) else: rules = ProductContentRule.objects.all() rules_data = list(rules.values()) return JsonResponse({ 'success': True, 'count': len(rules_data), 'rules': rules_data }) except Exception as e: logger.error(f"Error fetching content rules: {e}", exc_info=True) return JsonResponse({'error': str(e)}, status=500) def post(self, request): """Create a new content rule""" try: data = json.loads(request.body) required_fields = ['field_name'] if not all(field in data for field in required_fields): return JsonResponse({'error': 'field_name is required'}, status=400) # Create rule rule = ProductContentRule.objects.create( category=data.get('category'), field_name=data['field_name'], is_mandatory=data.get('is_mandatory', True), min_length=data.get('min_length'), max_length=data.get('max_length'), min_word_count=data.get('min_word_count'), max_word_count=data.get('max_word_count'), must_contain_keywords=data.get('must_contain_keywords', []), validation_regex=data.get('validation_regex', ''), description=data.get('description', '') ) # Clear cache if data.get('category'): cache.delete(f"content_rules_{data['category']}") return JsonResponse({ 'success': True, 'rule_id': rule.id, 'message': 'Content rule created successfully' }) except Exception as e: logger.error(f"Error creating content rule: {e}", exc_info=True) return JsonResponse({'error': str(e)}, status=500) @method_decorator(csrf_exempt, name='dispatch') class ProductScoreDetailView(View): """Get detailed score for a specific product""" def get(self, request, sku): try: product = get_object_or_404(Product, sku=sku) # Get latest score latest_score = AttributeScore.objects.filter(product=product).order_by('-created_at').first() if not latest_score: return JsonResponse({'error': 'No score found for this product'}, status=404) # Get interpretation scorer = AttributeQualityScorer() interpretation = scorer.get_score_interpretation(latest_score.score) # Categorize issues and suggestions categorized = categorize_issues_and_suggestions( latest_score.issues, latest_score.suggestions ) return JsonResponse({ 'success': True, 'product': { 'sku': product.sku, 'category': product.category, 'title': product.title, 'description': product.description, 'short_description': product.short_description, 'seo_title': product.seo_title, 'seo_description': product.seo_description, 'attributes': product.attributes }, 'score': { 'final_score': latest_score.score, 'max_score': latest_score.max_score, 'breakdown': latest_score.details, 'interpretation': interpretation }, 'categorized_feedback': categorized, # NEW: Categorized by component 'ai_suggestions': latest_score.ai_suggestions, 'scored_at': latest_score.created_at.isoformat(), # Keep original format for backward compatibility 'issues': latest_score.issues, 'suggestions': latest_score.suggestions }) except Exception as e: logger.error(f"Error fetching product score: {e}", exc_info=True) return JsonResponse({'error': str(e)}, status=500)