phase 8
This commit is contained in:
@@ -97,5 +97,237 @@ class LinkerService:
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
def process_product(self, content_id: int) -> Content:
|
||||
"""
|
||||
Process product content for linking (Phase 8).
|
||||
Enhanced linking for products: links to related products, categories, and service pages.
|
||||
|
||||
Args:
|
||||
content_id: Content ID to process (must be entity_type='product')
|
||||
|
||||
Returns:
|
||||
Updated Content instance
|
||||
"""
|
||||
try:
|
||||
content = Content.objects.get(id=content_id, entity_type='product')
|
||||
except Content.DoesNotExist:
|
||||
raise ValueError(f"Product content with id {content_id} does not exist")
|
||||
|
||||
# Use base process but with product-specific candidate finding
|
||||
account = content.account
|
||||
|
||||
# Check credits
|
||||
try:
|
||||
self.credit_service.check_credits(account, 'linking')
|
||||
except InsufficientCreditsError:
|
||||
raise
|
||||
|
||||
# Find product-specific link candidates (related products, categories, services)
|
||||
candidates = self._find_product_candidates(content)
|
||||
|
||||
if not candidates:
|
||||
logger.info(f"No link candidates found for product content {content_id}")
|
||||
return content
|
||||
|
||||
# Inject links
|
||||
result = self.injection_engine.inject_links(content, candidates)
|
||||
|
||||
# Update content
|
||||
content.html_content = result['html_content']
|
||||
content.internal_links = result['links']
|
||||
content.linker_version += 1
|
||||
content.save(update_fields=['html_content', 'internal_links', 'linker_version'])
|
||||
|
||||
# Deduct credits
|
||||
self.credit_service.deduct_credits_for_operation(
|
||||
account=account,
|
||||
operation_type='linking',
|
||||
description=f"Product linking for: {content.title or 'Untitled'}",
|
||||
related_object_type='content',
|
||||
related_object_id=content.id
|
||||
)
|
||||
|
||||
logger.info(f"Linked product content {content_id}: {result['links_added']} links added")
|
||||
|
||||
return content
|
||||
|
||||
def process_taxonomy(self, content_id: int) -> Content:
|
||||
"""
|
||||
Process taxonomy content for linking (Phase 8).
|
||||
Enhanced linking for taxonomies: links to related categories, tags, and content.
|
||||
|
||||
Args:
|
||||
content_id: Content ID to process (must be entity_type='taxonomy')
|
||||
|
||||
Returns:
|
||||
Updated Content instance
|
||||
"""
|
||||
try:
|
||||
content = Content.objects.get(id=content_id, entity_type='taxonomy')
|
||||
except Content.DoesNotExist:
|
||||
raise ValueError(f"Taxonomy content with id {content_id} does not exist")
|
||||
|
||||
# Use base process but with taxonomy-specific candidate finding
|
||||
account = content.account
|
||||
|
||||
# Check credits
|
||||
try:
|
||||
self.credit_service.check_credits(account, 'linking')
|
||||
except InsufficientCreditsError:
|
||||
raise
|
||||
|
||||
# Find taxonomy-specific link candidates (related taxonomies, categories, content)
|
||||
candidates = self._find_taxonomy_candidates(content)
|
||||
|
||||
if not candidates:
|
||||
logger.info(f"No link candidates found for taxonomy content {content_id}")
|
||||
return content
|
||||
|
||||
# Inject links
|
||||
result = self.injection_engine.inject_links(content, candidates)
|
||||
|
||||
# Update content
|
||||
content.html_content = result['html_content']
|
||||
content.internal_links = result['links']
|
||||
content.linker_version += 1
|
||||
content.save(update_fields=['html_content', 'internal_links', 'linker_version'])
|
||||
|
||||
# Deduct credits
|
||||
self.credit_service.deduct_credits_for_operation(
|
||||
account=account,
|
||||
operation_type='linking',
|
||||
description=f"Taxonomy linking for: {content.title or 'Untitled'}",
|
||||
related_object_type='content',
|
||||
related_object_id=content.id
|
||||
)
|
||||
|
||||
logger.info(f"Linked taxonomy content {content_id}: {result['links_added']} links added")
|
||||
|
||||
return content
|
||||
|
||||
def _find_product_candidates(self, content: Content) -> List[dict]:
|
||||
"""
|
||||
Find link candidates specific to product content.
|
||||
|
||||
Args:
|
||||
content: Product Content instance
|
||||
|
||||
Returns:
|
||||
List of candidate dicts
|
||||
"""
|
||||
candidates = []
|
||||
|
||||
# Find related products (same category, similar features)
|
||||
related_products = Content.objects.filter(
|
||||
account=content.account,
|
||||
site=content.site,
|
||||
sector=content.sector,
|
||||
entity_type='product',
|
||||
status__in=['draft', 'review', 'publish']
|
||||
).exclude(id=content.id)
|
||||
|
||||
# Use structure_data to find products with similar categories/features
|
||||
if content.structure_data:
|
||||
product_type = content.structure_data.get('product_type')
|
||||
if product_type:
|
||||
related_products = related_products.filter(
|
||||
structure_data__product_type=product_type
|
||||
)
|
||||
|
||||
# Add product candidates
|
||||
for product in related_products[:5]: # Limit to 5 related products
|
||||
candidates.append({
|
||||
'content_id': product.id,
|
||||
'title': product.title or 'Untitled Product',
|
||||
'url': f'/products/{product.id}', # Placeholder URL
|
||||
'relevance_score': 0.8,
|
||||
'anchor_text': product.title or 'Related Product'
|
||||
})
|
||||
|
||||
# Find related service pages
|
||||
related_services = Content.objects.filter(
|
||||
account=content.account,
|
||||
site=content.site,
|
||||
sector=content.sector,
|
||||
entity_type='service',
|
||||
status__in=['draft', 'review', 'publish']
|
||||
)[:3] # Limit to 3 related services
|
||||
|
||||
for service in related_services:
|
||||
candidates.append({
|
||||
'content_id': service.id,
|
||||
'title': service.title or 'Untitled Service',
|
||||
'url': f'/services/{service.id}', # Placeholder URL
|
||||
'relevance_score': 0.6,
|
||||
'anchor_text': service.title or 'Related Service'
|
||||
})
|
||||
|
||||
# Use base candidate engine for additional candidates
|
||||
base_candidates = self.candidate_engine.find_candidates(content, max_candidates=5)
|
||||
candidates.extend(base_candidates)
|
||||
|
||||
return candidates
|
||||
|
||||
def _find_taxonomy_candidates(self, content: Content) -> List[dict]:
|
||||
"""
|
||||
Find link candidates specific to taxonomy content.
|
||||
|
||||
Args:
|
||||
content: Taxonomy Content instance
|
||||
|
||||
Returns:
|
||||
List of candidate dicts
|
||||
"""
|
||||
candidates = []
|
||||
|
||||
# Find related taxonomies
|
||||
related_taxonomies = Content.objects.filter(
|
||||
account=content.account,
|
||||
site=content.site,
|
||||
sector=content.sector,
|
||||
entity_type='taxonomy',
|
||||
status__in=['draft', 'review', 'publish']
|
||||
).exclude(id=content.id)[:5] # Limit to 5 related taxonomies
|
||||
|
||||
for taxonomy in related_taxonomies:
|
||||
candidates.append({
|
||||
'content_id': taxonomy.id,
|
||||
'title': taxonomy.title or 'Untitled Taxonomy',
|
||||
'url': f'/taxonomy/{taxonomy.id}', # Placeholder URL
|
||||
'relevance_score': 0.7,
|
||||
'anchor_text': taxonomy.title or 'Related Taxonomy'
|
||||
})
|
||||
|
||||
# Find content in this taxonomy (using json_blocks categories/tags)
|
||||
if content.json_blocks:
|
||||
for block in content.json_blocks:
|
||||
if block.get('type') == 'categories':
|
||||
categories = block.get('items', [])
|
||||
for category in categories[:3]: # Limit to 3 categories
|
||||
category_name = category.get('name', '')
|
||||
if category_name:
|
||||
related_content = Content.objects.filter(
|
||||
account=content.account,
|
||||
site=content.site,
|
||||
sector=content.sector,
|
||||
categories__icontains=category_name,
|
||||
status__in=['draft', 'review', 'publish']
|
||||
).exclude(id=content.id)[:3]
|
||||
|
||||
for related in related_content:
|
||||
candidates.append({
|
||||
'content_id': related.id,
|
||||
'title': related.title or 'Untitled',
|
||||
'url': f'/content/{related.id}', # Placeholder URL
|
||||
'relevance_score': 0.6,
|
||||
'anchor_text': related.title or 'Related Content'
|
||||
})
|
||||
|
||||
# Use base candidate engine for additional candidates
|
||||
base_candidates = self.candidate_engine.find_candidates(content, max_candidates=5)
|
||||
candidates.extend(base_candidates)
|
||||
|
||||
return candidates
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Tests for Universal Content Types Linking (Phase 8)
|
||||
Tests for product and taxonomy linking
|
||||
"""
|
||||
from unittest.mock import patch, MagicMock
|
||||
from django.test import TestCase
|
||||
from igny8_core.business.content.models import Content
|
||||
from igny8_core.business.linking.services.linker_service import LinkerService
|
||||
from igny8_core.api.tests.test_integration_base import IntegrationTestBase
|
||||
|
||||
|
||||
class UniversalContentLinkingTests(IntegrationTestBase):
|
||||
"""Tests for Phase 8: Universal Content Types Linking"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.linker_service = LinkerService()
|
||||
|
||||
# Create product content
|
||||
self.product_content = Content.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
title='Test Product',
|
||||
html_content='<p>Product content with features and specifications.</p>',
|
||||
entity_type='product',
|
||||
json_blocks=[
|
||||
{'type': 'features', 'heading': 'Features', 'items': ['Feature 1', 'Feature 2']}
|
||||
],
|
||||
structure_data={'product_type': 'software'},
|
||||
word_count=1500,
|
||||
status='draft'
|
||||
)
|
||||
|
||||
# Create related product
|
||||
self.related_product = Content.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
title='Related Product',
|
||||
html_content='<p>Related product content.</p>',
|
||||
entity_type='product',
|
||||
structure_data={'product_type': 'software'},
|
||||
word_count=1500,
|
||||
status='draft'
|
||||
)
|
||||
|
||||
# Create service content
|
||||
self.service_content = Content.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
title='Related Service',
|
||||
html_content='<p>Service content.</p>',
|
||||
entity_type='service',
|
||||
word_count=1800,
|
||||
status='draft'
|
||||
)
|
||||
|
||||
# Create taxonomy content
|
||||
self.taxonomy_content = Content.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
title='Test Taxonomy',
|
||||
html_content='<p>Taxonomy content with categories.</p>',
|
||||
entity_type='taxonomy',
|
||||
json_blocks=[
|
||||
{
|
||||
'type': 'categories',
|
||||
'heading': 'Categories',
|
||||
'items': [
|
||||
{'name': 'Category 1', 'description': 'Desc 1', 'subcategories': []}
|
||||
]
|
||||
}
|
||||
],
|
||||
word_count=1200,
|
||||
status='draft'
|
||||
)
|
||||
|
||||
# Create related taxonomy
|
||||
self.related_taxonomy = Content.objects.create(
|
||||
account=self.account,
|
||||
site=self.site,
|
||||
sector=self.sector,
|
||||
title='Related Taxonomy',
|
||||
html_content='<p>Related taxonomy content.</p>',
|
||||
entity_type='taxonomy',
|
||||
word_count=1200,
|
||||
status='draft'
|
||||
)
|
||||
|
||||
@patch('igny8_core.business.linking.services.linker_service.InjectionEngine.inject_links')
|
||||
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
|
||||
@patch('igny8_core.business.linking.services.linker_service.CreditService.deduct_credits_for_operation')
|
||||
def test_linking_works_for_products(self, mock_deduct, mock_check_credits, mock_inject_links):
|
||||
"""
|
||||
Test: Linking works for all content types (products, taxonomies)
|
||||
Task 20: Verify product linking finds related products and services
|
||||
"""
|
||||
# Mock injection engine
|
||||
mock_inject_links.return_value = {
|
||||
'html_content': '<p>Product content with links.</p>',
|
||||
'links': [
|
||||
{'content_id': self.related_product.id, 'anchor_text': 'Related Product'},
|
||||
{'content_id': self.service_content.id, 'anchor_text': 'Related Service'}
|
||||
],
|
||||
'links_added': 2
|
||||
}
|
||||
|
||||
# Process product linking
|
||||
result = self.linker_service.process_product(self.product_content.id)
|
||||
|
||||
# Verify result
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.entity_type, 'product')
|
||||
self.assertIsNotNone(result.internal_links)
|
||||
self.assertEqual(len(result.internal_links), 2)
|
||||
self.assertEqual(result.linker_version, 1)
|
||||
|
||||
# Verify injection was called
|
||||
mock_inject_links.assert_called_once()
|
||||
candidates = mock_inject_links.call_args[0][1]
|
||||
self.assertGreater(len(candidates), 0)
|
||||
|
||||
# Verify product candidates were found
|
||||
product_candidates = [c for c in candidates if c.get('content_id') == self.related_product.id]
|
||||
self.assertGreater(len(product_candidates), 0)
|
||||
|
||||
@patch('igny8_core.business.linking.services.linker_service.InjectionEngine.inject_links')
|
||||
@patch('igny8_core.business.linking.services.linker_service.CreditService.check_credits')
|
||||
@patch('igny8_core.business.linking.services.linker_service.CreditService.deduct_credits_for_operation')
|
||||
def test_linking_works_for_taxonomies(self, mock_deduct, mock_check_credits, mock_inject_links):
|
||||
"""
|
||||
Test: Linking works for all content types (products, taxonomies)
|
||||
Task 20: Verify taxonomy linking finds related taxonomies and content
|
||||
"""
|
||||
# Mock injection engine
|
||||
mock_inject_links.return_value = {
|
||||
'html_content': '<p>Taxonomy content with links.</p>',
|
||||
'links': [
|
||||
{'content_id': self.related_taxonomy.id, 'anchor_text': 'Related Taxonomy'}
|
||||
],
|
||||
'links_added': 1
|
||||
}
|
||||
|
||||
# Process taxonomy linking
|
||||
result = self.linker_service.process_taxonomy(self.taxonomy_content.id)
|
||||
|
||||
# Verify result
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result.entity_type, 'taxonomy')
|
||||
self.assertIsNotNone(result.internal_links)
|
||||
self.assertEqual(len(result.internal_links), 1)
|
||||
self.assertEqual(result.linker_version, 1)
|
||||
|
||||
# Verify injection was called
|
||||
mock_inject_links.assert_called_once()
|
||||
candidates = mock_inject_links.call_args[0][1]
|
||||
self.assertGreater(len(candidates), 0)
|
||||
|
||||
# Verify taxonomy candidates were found
|
||||
taxonomy_candidates = [c for c in candidates if c.get('content_id') == self.related_taxonomy.id]
|
||||
self.assertGreater(len(taxonomy_candidates), 0)
|
||||
|
||||
def test_product_linking_finds_related_products(self):
|
||||
"""
|
||||
Test: Linking works for all content types (products, taxonomies)
|
||||
Task 20: Verify _find_product_candidates finds related products
|
||||
"""
|
||||
candidates = self.linker_service._find_product_candidates(self.product_content)
|
||||
|
||||
# Should find related product
|
||||
product_ids = [c['content_id'] for c in candidates]
|
||||
self.assertIn(self.related_product.id, product_ids)
|
||||
|
||||
# Should find related service
|
||||
self.assertIn(self.service_content.id, product_ids)
|
||||
|
||||
def test_taxonomy_linking_finds_related_taxonomies(self):
|
||||
"""
|
||||
Test: Linking works for all content types (products, taxonomies)
|
||||
Task 20: Verify _find_taxonomy_candidates finds related taxonomies
|
||||
"""
|
||||
candidates = self.linker_service._find_taxonomy_candidates(self.taxonomy_content)
|
||||
|
||||
# Should find related taxonomy
|
||||
taxonomy_ids = [c['content_id'] for c in candidates]
|
||||
self.assertIn(self.related_taxonomy.id, taxonomy_ids)
|
||||
|
||||
Reference in New Issue
Block a user