- Enhanced the `error_response` function to support backward compatibility by normalizing arguments when positional arguments are misused. - Updated various views to pass `None` for the `errors` parameter in `error_response` calls, ensuring consistent response formatting. - Adjusted logging in `ContentSyncService` and `WordPressClient` to use debug level for expected 401 errors, improving log clarity. - Removed deprecated fields from serializers and views, streamlining content management processes.
602 lines
21 KiB
Python
602 lines
21 KiB
Python
"""
|
|
WordPress Integration Service
|
|
Handles communication with WordPress sites via REST API
|
|
Stage 4: Enhanced with taxonomy and WooCommerce product support
|
|
"""
|
|
import logging
|
|
import requests
|
|
from typing import Dict, Any, Optional, List
|
|
from django.conf import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WordPressClient:
|
|
"""
|
|
WordPress REST API client for content publishing and sync.
|
|
"""
|
|
|
|
def __init__(self, site_url: str, username: str = None, app_password: str = None):
|
|
"""
|
|
Initialize WordPress client.
|
|
|
|
Args:
|
|
site_url: WordPress site URL (e.g., https://example.com)
|
|
username: WordPress username or application password username
|
|
app_password: WordPress application password
|
|
"""
|
|
self.site_url = site_url.rstrip('/')
|
|
self.api_base = f"{self.site_url}/wp-json/wp/v2"
|
|
self.woocommerce_api_base = f"{self.site_url}/wp-json/wc/v3" # WooCommerce API
|
|
self.igny8_api_base = f"{self.site_url}/wp-json/igny8/v1" # Custom IGNY8 endpoints
|
|
self.username = username
|
|
self.app_password = app_password
|
|
self.session = requests.Session()
|
|
|
|
# Set up authentication if provided
|
|
if username and app_password:
|
|
self.session.auth = (username, app_password)
|
|
|
|
def test_connection(self) -> Dict[str, Any]:
|
|
"""
|
|
Test connection to WordPress site.
|
|
|
|
Returns:
|
|
Dict with 'success', 'message', 'wp_version'
|
|
"""
|
|
try:
|
|
response = self.session.get(f"{self.api_base}/")
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'message': 'Connection successful',
|
|
'wp_version': data.get('version', 'Unknown'),
|
|
}
|
|
return {
|
|
'success': False,
|
|
'message': f"HTTP {response.status_code}",
|
|
'wp_version': None,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress connection test failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'message': str(e),
|
|
'wp_version': None,
|
|
}
|
|
|
|
def create_post(
|
|
self,
|
|
title: str,
|
|
content: str,
|
|
status: str = 'draft',
|
|
featured_image_url: Optional[str] = None,
|
|
**kwargs
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a new WordPress post.
|
|
|
|
Args:
|
|
title: Post title
|
|
content: Post content (HTML or blocks)
|
|
status: Post status ('draft', 'publish', 'pending')
|
|
featured_image_url: URL of featured image (must be uploaded first)
|
|
**kwargs: Additional post fields (excerpt, categories, etc.)
|
|
|
|
Returns:
|
|
Dict with 'success', 'post_id', 'url', 'error'
|
|
"""
|
|
try:
|
|
post_data = {
|
|
'title': title,
|
|
'content': content,
|
|
'status': status,
|
|
**kwargs
|
|
}
|
|
|
|
if featured_image_url:
|
|
# Convert URL to media ID if needed
|
|
media_id = self._get_media_id_from_url(featured_image_url)
|
|
if media_id:
|
|
post_data['featured_media'] = media_id
|
|
|
|
response = self.session.post(f"{self.api_base}/posts", json=post_data)
|
|
|
|
if response.status_code in [200, 201]:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'post_id': data.get('id'),
|
|
'url': data.get('link'),
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'post_id': None,
|
|
'url': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress post creation failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'post_id': None,
|
|
'url': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
def upload_image(self, image_url: str, filename: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Upload an image to WordPress media library.
|
|
|
|
Args:
|
|
image_url: URL of image to upload
|
|
filename: Optional filename
|
|
|
|
Returns:
|
|
Dict with 'success', 'media_id', 'url', 'error'
|
|
"""
|
|
try:
|
|
# Download image
|
|
img_response = requests.get(image_url)
|
|
if img_response.status_code != 200:
|
|
return {
|
|
'success': False,
|
|
'media_id': None,
|
|
'url': None,
|
|
'error': f"Failed to download image: HTTP {img_response.status_code}",
|
|
}
|
|
|
|
# Upload to WordPress
|
|
files = {
|
|
'file': (filename or 'image.jpg', img_response.content, img_response.headers.get('content-type', 'image/jpeg'))
|
|
}
|
|
|
|
response = self.session.post(f"{self.api_base}/media", files=files)
|
|
|
|
if response.status_code in [200, 201]:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'media_id': data.get('id'),
|
|
'url': data.get('source_url'),
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'media_id': None,
|
|
'url': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress image upload failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'media_id': None,
|
|
'url': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
def _get_media_id_from_url(self, url: str) -> Optional[int]:
|
|
"""Helper to get media ID from URL (if already uploaded)."""
|
|
# TODO: Implement media lookup by URL
|
|
return None
|
|
|
|
def sync_settings(self, settings_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Sync settings to WordPress via custom IGNY8 endpoint.
|
|
|
|
Args:
|
|
settings_data: Settings dictionary to sync
|
|
|
|
Returns:
|
|
Dict with 'success', 'message', 'error'
|
|
"""
|
|
try:
|
|
response = self.session.post(
|
|
f"{self.igny8_api_base}/sync-settings",
|
|
json=settings_data
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return {
|
|
'success': True,
|
|
'message': 'Settings synced successfully',
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'message': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress settings sync failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'message': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
# Stage 4: Taxonomy Methods
|
|
|
|
def get_categories(self, per_page: int = 100, page: int = 1) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get WordPress categories.
|
|
|
|
Args:
|
|
per_page: Number of categories per page
|
|
page: Page number
|
|
|
|
Returns:
|
|
List of category dictionaries
|
|
"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.api_base}/categories",
|
|
params={'per_page': per_page, 'page': page}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
categories = response.json()
|
|
return [
|
|
{
|
|
'id': cat.get('id'),
|
|
'name': cat.get('name', ''),
|
|
'slug': cat.get('slug', ''),
|
|
'description': cat.get('description', ''),
|
|
'count': cat.get('count', 0),
|
|
'parent': cat.get('parent', 0),
|
|
'link': cat.get('link', '')
|
|
}
|
|
for cat in categories
|
|
]
|
|
logger.warning(f"Failed to fetch categories: HTTP {response.status_code}")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Error fetching WordPress categories: {e}")
|
|
return []
|
|
|
|
def create_category(
|
|
self,
|
|
name: str,
|
|
slug: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
parent_id: Optional[int] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a WordPress category.
|
|
|
|
Args:
|
|
name: Category name
|
|
slug: Category slug (auto-generated if not provided)
|
|
description: Category description
|
|
parent_id: Parent category ID
|
|
|
|
Returns:
|
|
Dict with 'success', 'category_id', 'error'
|
|
"""
|
|
try:
|
|
category_data = {'name': name}
|
|
if slug:
|
|
category_data['slug'] = slug
|
|
if description:
|
|
category_data['description'] = description
|
|
if parent_id:
|
|
category_data['parent'] = parent_id
|
|
|
|
response = self.session.post(
|
|
f"{self.api_base}/categories",
|
|
json=category_data
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'category_id': data.get('id'),
|
|
'slug': data.get('slug'),
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'category_id': None,
|
|
'slug': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress category creation failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'category_id': None,
|
|
'slug': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
def get_tags(self, per_page: int = 100, page: int = 1) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get WordPress tags.
|
|
|
|
Args:
|
|
per_page: Number of tags per page
|
|
page: Page number
|
|
|
|
Returns:
|
|
List of tag dictionaries
|
|
"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.api_base}/tags",
|
|
params={'per_page': per_page, 'page': page}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
tags = response.json()
|
|
return [
|
|
{
|
|
'id': tag.get('id'),
|
|
'name': tag.get('name', ''),
|
|
'slug': tag.get('slug', ''),
|
|
'description': tag.get('description', ''),
|
|
'count': tag.get('count', 0),
|
|
'link': tag.get('link', '')
|
|
}
|
|
for tag in tags
|
|
]
|
|
logger.warning(f"Failed to fetch tags: HTTP {response.status_code}")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Error fetching WordPress tags: {e}")
|
|
return []
|
|
|
|
def create_tag(
|
|
self,
|
|
name: str,
|
|
slug: Optional[str] = None,
|
|
description: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a WordPress tag.
|
|
|
|
Args:
|
|
name: Tag name
|
|
slug: Tag slug (auto-generated if not provided)
|
|
description: Tag description
|
|
|
|
Returns:
|
|
Dict with 'success', 'tag_id', 'error'
|
|
"""
|
|
try:
|
|
tag_data = {'name': name}
|
|
if slug:
|
|
tag_data['slug'] = slug
|
|
if description:
|
|
tag_data['description'] = description
|
|
|
|
response = self.session.post(
|
|
f"{self.api_base}/tags",
|
|
json=tag_data
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'tag_id': data.get('id'),
|
|
'slug': data.get('slug'),
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'tag_id': None,
|
|
'slug': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WordPress tag creation failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'tag_id': None,
|
|
'slug': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
# Stage 4: WooCommerce Product Methods
|
|
|
|
def get_products(
|
|
self,
|
|
per_page: int = 100,
|
|
page: int = 1,
|
|
status: str = 'publish'
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get WooCommerce products.
|
|
|
|
Args:
|
|
per_page: Number of products per page
|
|
page: Page number
|
|
status: Product status ('publish', 'draft', etc.)
|
|
|
|
Returns:
|
|
List of product dictionaries
|
|
"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.woocommerce_api_base}/products",
|
|
params={
|
|
'per_page': per_page,
|
|
'page': page,
|
|
'status': status
|
|
}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
products = response.json()
|
|
return [
|
|
{
|
|
'id': prod.get('id'),
|
|
'name': prod.get('name', ''),
|
|
'slug': prod.get('slug', ''),
|
|
'sku': prod.get('sku', ''),
|
|
'type': prod.get('type', 'simple'),
|
|
'status': prod.get('status', 'publish'),
|
|
'description': prod.get('description', ''),
|
|
'short_description': prod.get('short_description', ''),
|
|
'price': prod.get('price', ''),
|
|
'regular_price': prod.get('regular_price', ''),
|
|
'sale_price': prod.get('sale_price', ''),
|
|
'on_sale': prod.get('on_sale', False),
|
|
'stock_status': prod.get('stock_status', 'instock'),
|
|
'stock_quantity': prod.get('stock_quantity'),
|
|
'categories': prod.get('categories', []),
|
|
'tags': prod.get('tags', []),
|
|
'images': prod.get('images', []),
|
|
'attributes': prod.get('attributes', []),
|
|
'variations': prod.get('variations', []),
|
|
'permalink': prod.get('permalink', '')
|
|
}
|
|
for prod in products
|
|
]
|
|
# Log as debug if 401 (expected if WooCommerce not configured)
|
|
if response.status_code == 401:
|
|
logger.debug(f"WooCommerce products require authentication: HTTP {response.status_code}")
|
|
else:
|
|
logger.warning(f"Failed to fetch products: HTTP {response.status_code}")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Error fetching WooCommerce products: {e}")
|
|
return []
|
|
|
|
def create_product(self, product_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Create a WooCommerce product.
|
|
|
|
Args:
|
|
product_data: Product data dictionary
|
|
|
|
Returns:
|
|
Dict with 'success', 'product_id', 'error'
|
|
"""
|
|
try:
|
|
response = self.session.post(
|
|
f"{self.woocommerce_api_base}/products",
|
|
json=product_data
|
|
)
|
|
|
|
if response.status_code in [200, 201]:
|
|
data = response.json()
|
|
return {
|
|
'success': True,
|
|
'product_id': data.get('id'),
|
|
'slug': data.get('slug'),
|
|
'error': None,
|
|
}
|
|
return {
|
|
'success': False,
|
|
'product_id': None,
|
|
'slug': None,
|
|
'error': f"HTTP {response.status_code}: {response.text}",
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"WooCommerce product creation failed: {e}")
|
|
return {
|
|
'success': False,
|
|
'product_id': None,
|
|
'slug': None,
|
|
'error': str(e),
|
|
}
|
|
|
|
def get_product_categories(self, per_page: int = 100, page: int = 1) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get WooCommerce product categories.
|
|
|
|
Args:
|
|
per_page: Number of categories per page
|
|
page: Page number
|
|
|
|
Returns:
|
|
List of product category dictionaries
|
|
"""
|
|
try:
|
|
response = self.session.get(
|
|
f"{self.woocommerce_api_base}/products/categories",
|
|
params={'per_page': per_page, 'page': page}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
categories = response.json()
|
|
return [
|
|
{
|
|
'id': cat.get('id'),
|
|
'name': cat.get('name', ''),
|
|
'slug': cat.get('slug', ''),
|
|
'description': cat.get('description', ''),
|
|
'count': cat.get('count', 0),
|
|
'parent': cat.get('parent', 0),
|
|
'image': cat.get('image', {}).get('src') if cat.get('image') else None
|
|
}
|
|
for cat in categories
|
|
]
|
|
# Log as debug if 401 (expected if WooCommerce not configured)
|
|
if response.status_code == 401:
|
|
logger.debug(f"WooCommerce product categories require authentication: HTTP {response.status_code}")
|
|
else:
|
|
logger.warning(f"Failed to fetch product categories: HTTP {response.status_code}")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Error fetching WooCommerce product categories: {e}")
|
|
return []
|
|
|
|
def get_product_attributes(self) -> List[Dict[str, Any]]:
|
|
"""
|
|
Get WooCommerce product attributes.
|
|
|
|
Returns:
|
|
List of product attribute dictionaries with terms
|
|
"""
|
|
try:
|
|
# Get attributes
|
|
response = self.session.get(f"{self.woocommerce_api_base}/products/attributes")
|
|
|
|
if response.status_code != 200:
|
|
logger.warning(f"Failed to fetch product attributes: HTTP {response.status_code}")
|
|
return []
|
|
|
|
attributes = response.json()
|
|
result = []
|
|
|
|
for attr in attributes:
|
|
attr_id = attr.get('id')
|
|
# Get terms for this attribute
|
|
terms_response = self.session.get(
|
|
f"{self.woocommerce_api_base}/products/attributes/{attr_id}/terms"
|
|
)
|
|
|
|
terms = []
|
|
if terms_response.status_code == 200:
|
|
terms_data = terms_response.json()
|
|
terms = [
|
|
{
|
|
'id': term.get('id'),
|
|
'name': term.get('name', ''),
|
|
'slug': term.get('slug', '')
|
|
}
|
|
for term in terms_data
|
|
]
|
|
|
|
result.append({
|
|
'id': attr_id,
|
|
'name': attr.get('name', ''),
|
|
'slug': attr.get('slug', ''),
|
|
'type': attr.get('type', 'select'),
|
|
'order_by': attr.get('order_by', 'menu_order'),
|
|
'has_archives': attr.get('has_archives', False),
|
|
'terms': terms
|
|
})
|
|
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error fetching WooCommerce product attributes: {e}")
|
|
return []
|
|
|