docs cleanup
This commit is contained in:
@@ -1,406 +0,0 @@
|
||||
# Admin & Views Update Summary
|
||||
|
||||
**Date**: November 21, 2025
|
||||
**Status**: ✅ **COMPLETED**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Updated all Django admin interfaces, API views, filters, and serializers to use the new unified content architecture.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Writer Module Updates
|
||||
|
||||
### Admin (`igny8_core/modules/writer/admin.py`)
|
||||
|
||||
#### 1. **TasksAdmin** - Simplified & Deprecated Fields Marked
|
||||
```python
|
||||
list_display = ['title', 'site', 'sector', 'status', 'cluster', 'created_at']
|
||||
list_filter = ['status', 'site', 'sector', 'cluster']
|
||||
readonly_fields = ['content_type', 'content_structure', 'entity_type', 'cluster_role', 'assigned_post_id', 'post_url']
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Removed `content_type` and `word_count` from list display
|
||||
- Added fieldsets with "Deprecated Fields" section (collapsed)
|
||||
- Marked 6 deprecated fields as read-only
|
||||
|
||||
#### 2. **ContentAdmin** - Enhanced with New Structure
|
||||
```python
|
||||
list_display = ['title', 'entity_type', 'content_format', 'cluster_role', 'site', 'sector', 'source', 'sync_status', 'word_count', 'generated_at']
|
||||
list_filter = ['entity_type', 'content_format', 'cluster_role', 'source', 'sync_status', 'status', 'site', 'sector', 'generated_at']
|
||||
filter_horizontal = ['taxonomies']
|
||||
readonly_fields = ['categories', 'tags']
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Added `entity_type`, `content_format`, `cluster_role` to list display
|
||||
- Added `source`, `sync_status` filters
|
||||
- Added `taxonomies` M2M widget (filter_horizontal)
|
||||
- Organized into 7 fieldsets:
|
||||
- Basic Info
|
||||
- Content Classification
|
||||
- Content
|
||||
- SEO
|
||||
- Taxonomies & Attributes
|
||||
- WordPress Sync
|
||||
- Optimization
|
||||
- Deprecated Fields (collapsed)
|
||||
|
||||
#### 3. **ContentTaxonomyAdmin** - NEW
|
||||
```python
|
||||
list_display = ['name', 'taxonomy_type', 'slug', 'parent', 'external_id', 'external_taxonomy', 'sync_status', 'count', 'site', 'sector']
|
||||
list_filter = ['taxonomy_type', 'sync_status', 'site', 'sector', 'parent']
|
||||
filter_horizontal = ['clusters']
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Full CRUD for categories, tags, product attributes
|
||||
- WordPress sync fields visible
|
||||
- Semantic cluster mapping via M2M widget
|
||||
- Hierarchical taxonomy support (parent field)
|
||||
|
||||
#### 4. **ContentAttributeAdmin** - NEW
|
||||
```python
|
||||
list_display = ['name', 'value', 'attribute_type', 'content', 'cluster', 'external_id', 'source', 'site', 'sector']
|
||||
list_filter = ['attribute_type', 'source', 'site', 'sector']
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Product specs, service modifiers, semantic facets
|
||||
- WordPress/WooCommerce sync fields
|
||||
- Link to content or cluster
|
||||
|
||||
---
|
||||
|
||||
### Views (`igny8_core/modules/writer/views.py`)
|
||||
|
||||
#### 1. **TasksViewSet** - Simplified Filters
|
||||
```python
|
||||
filterset_fields = ['status', 'cluster_id'] # Removed deprecated fields
|
||||
```
|
||||
|
||||
#### 2. **ContentViewSet** - Enhanced Filters
|
||||
```python
|
||||
queryset = Content.objects.select_related('task', 'site', 'sector', 'cluster').prefetch_related('taxonomies', 'attributes')
|
||||
filterset_fields = [
|
||||
'task_id',
|
||||
'status',
|
||||
'entity_type', # NEW
|
||||
'content_format', # NEW
|
||||
'cluster_role', # NEW
|
||||
'source', # NEW
|
||||
'sync_status', # NEW
|
||||
'cluster',
|
||||
'external_type', # NEW
|
||||
]
|
||||
search_fields = ['title', 'meta_title', 'primary_keyword', 'external_url'] # Added external_url
|
||||
ordering_fields = ['generated_at', 'updated_at', 'word_count', 'status', 'entity_type', 'content_format']
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Added 5 new filter fields for unified structure
|
||||
- Optimized queryset with select_related & prefetch_related
|
||||
- Added external_url to search fields
|
||||
|
||||
#### 3. **ContentTaxonomyViewSet** - NEW
|
||||
```python
|
||||
Endpoint: /api/v1/writer/taxonomies/
|
||||
Methods: GET, POST, PUT, PATCH, DELETE
|
||||
|
||||
filterset_fields = ['taxonomy_type', 'sync_status', 'parent', 'external_id', 'external_taxonomy']
|
||||
search_fields = ['name', 'slug', 'description', 'external_taxonomy']
|
||||
ordering = ['taxonomy_type', 'name']
|
||||
```
|
||||
|
||||
**Custom Actions:**
|
||||
- `POST /api/v1/writer/taxonomies/{id}/map_to_cluster/` - Map taxonomy to semantic cluster
|
||||
- `GET /api/v1/writer/taxonomies/{id}/contents/` - Get all content for taxonomy
|
||||
|
||||
#### 4. **ContentAttributeViewSet** - NEW
|
||||
```python
|
||||
Endpoint: /api/v1/writer/attributes/
|
||||
Methods: GET, POST, PUT, PATCH, DELETE
|
||||
|
||||
filterset_fields = ['attribute_type', 'source', 'content', 'cluster', 'external_id']
|
||||
search_fields = ['name', 'value', 'external_attribute_name', 'content__title']
|
||||
ordering = ['attribute_type', 'name']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### URLs (`igny8_core/modules/writer/urls.py`)
|
||||
|
||||
**New Routes Added:**
|
||||
```python
|
||||
router.register(r'taxonomies', ContentTaxonomyViewSet, basename='taxonomy')
|
||||
router.register(r'attributes', ContentAttributeViewSet, basename='attribute')
|
||||
```
|
||||
|
||||
**Available Endpoints:**
|
||||
- `/api/v1/writer/tasks/`
|
||||
- `/api/v1/writer/images/`
|
||||
- `/api/v1/writer/content/`
|
||||
- `/api/v1/writer/taxonomies/` ✨ NEW
|
||||
- `/api/v1/writer/attributes/` ✨ NEW
|
||||
|
||||
---
|
||||
|
||||
## ✅ Planner Module Updates
|
||||
|
||||
### Admin (`igny8_core/modules/planner/admin.py`)
|
||||
|
||||
#### **ContentIdeasAdmin** - Updated for New Structure
|
||||
```python
|
||||
list_display = ['idea_title', 'site', 'sector', 'description_preview', 'site_entity_type', 'cluster_role', 'status', 'keyword_cluster', 'estimated_word_count', 'created_at']
|
||||
list_filter = ['status', 'site_entity_type', 'cluster_role', 'site', 'sector']
|
||||
readonly_fields = ['content_structure', 'content_type']
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Replaced `content_structure`, `content_type` with `site_entity_type`, `cluster_role` in display
|
||||
- Marked old fields as read-only in collapsed fieldset
|
||||
- Updated filters to use new fields
|
||||
|
||||
**Fieldsets:**
|
||||
- Basic Info
|
||||
- Content Planning (site_entity_type, cluster_role)
|
||||
- Keywords & Clustering
|
||||
- Deprecated Fields (collapsed)
|
||||
|
||||
---
|
||||
|
||||
### Views (`igny8_core/modules/planner/views.py`)
|
||||
|
||||
#### **ContentIdeasViewSet** - Updated Filters
|
||||
```python
|
||||
filterset_fields = ['status', 'keyword_cluster_id', 'site_entity_type', 'cluster_role'] # Updated
|
||||
```
|
||||
|
||||
**Changes:**
|
||||
- Replaced `content_structure`, `content_type` with `site_entity_type`, `cluster_role`
|
||||
|
||||
---
|
||||
|
||||
## 📊 New API Endpoints Summary
|
||||
|
||||
### Writer Taxonomies
|
||||
```bash
|
||||
GET /api/v1/writer/taxonomies/ # List all taxonomies
|
||||
POST /api/v1/writer/taxonomies/ # Create taxonomy
|
||||
GET /api/v1/writer/taxonomies/{id}/ # Get taxonomy
|
||||
PUT /api/v1/writer/taxonomies/{id}/ # Update taxonomy
|
||||
DELETE /api/v1/writer/taxonomies/{id}/ # Delete taxonomy
|
||||
POST /api/v1/writer/taxonomies/{id}/map_to_cluster/ # Map to cluster
|
||||
GET /api/v1/writer/taxonomies/{id}/contents/ # Get taxonomy contents
|
||||
```
|
||||
|
||||
**Filters:**
|
||||
- `?taxonomy_type=category` (category, tag, product_cat, product_tag, product_attr, service_cat)
|
||||
- `?sync_status=imported` (native, imported, synced)
|
||||
- `?parent=5` (hierarchical filtering)
|
||||
- `?external_id=12` (WP term ID)
|
||||
- `?external_taxonomy=category` (WP taxonomy name)
|
||||
|
||||
**Search:**
|
||||
- `?search=SEO` (searches name, slug, description)
|
||||
|
||||
---
|
||||
|
||||
### Writer Attributes
|
||||
```bash
|
||||
GET /api/v1/writer/attributes/ # List all attributes
|
||||
POST /api/v1/writer/attributes/ # Create attribute
|
||||
GET /api/v1/writer/attributes/{id}/ # Get attribute
|
||||
PUT /api/v1/writer/attributes/{id}/ # Update attribute
|
||||
DELETE /api/v1/writer/attributes/{id}/ # Delete attribute
|
||||
```
|
||||
|
||||
**Filters:**
|
||||
- `?attribute_type=product_spec` (product_spec, service_modifier, semantic_facet)
|
||||
- `?source=wordpress` (blueprint, manual, import, wordpress)
|
||||
- `?content=42` (filter by content ID)
|
||||
- `?cluster=8` (filter by cluster ID)
|
||||
- `?external_id=101` (WP attribute term ID)
|
||||
|
||||
**Search:**
|
||||
- `?search=Color` (searches name, value, external_attribute_name, content title)
|
||||
|
||||
---
|
||||
|
||||
### Enhanced Content Filters
|
||||
```bash
|
||||
GET /api/v1/writer/content/?entity_type=post
|
||||
GET /api/v1/writer/content/?content_format=listicle
|
||||
GET /api/v1/writer/content/?cluster_role=hub
|
||||
GET /api/v1/writer/content/?source=wordpress
|
||||
GET /api/v1/writer/content/?sync_status=imported
|
||||
GET /api/v1/writer/content/?external_type=product
|
||||
GET /api/v1/writer/content/?search=seo+tools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Backward Compatibility
|
||||
|
||||
### Deprecated Fields Still Work
|
||||
|
||||
**Tasks:**
|
||||
- `content_type`, `content_structure` → Read-only in admin
|
||||
- Still in database, marked with help text
|
||||
|
||||
**Content:**
|
||||
- `categories`, `tags` (JSON) → Read-only in admin
|
||||
- Data migrated to `taxonomies` M2M
|
||||
- Old fields preserved for transition period
|
||||
|
||||
**ContentIdeas:**
|
||||
- `content_structure`, `content_type` → Read-only in admin
|
||||
- Replaced by `site_entity_type`, `cluster_role`
|
||||
|
||||
---
|
||||
|
||||
## 📝 Django Admin Features
|
||||
|
||||
### New Admin Capabilities
|
||||
|
||||
1. **Content Taxonomy Management**
|
||||
- Create/edit categories, tags, product attributes
|
||||
- Map to semantic clusters (M2M widget)
|
||||
- View WordPress sync status
|
||||
- Hierarchical taxonomy support
|
||||
|
||||
2. **Content Attribute Management**
|
||||
- Create product specs (Color: Blue, Size: Large)
|
||||
- Create service modifiers (Location: NYC)
|
||||
- Create semantic facets (Target Audience: Enterprise)
|
||||
- Link to content or clusters
|
||||
|
||||
3. **Enhanced Content Admin**
|
||||
- Filter by entity_type, content_format, cluster_role
|
||||
- Filter by source (igny8, wordpress, shopify)
|
||||
- Filter by sync_status (native, imported, synced)
|
||||
- Assign taxonomies via M2M widget
|
||||
- View WordPress sync metadata
|
||||
|
||||
4. **Simplified Task Admin**
|
||||
- Deprecated fields hidden in collapsed section
|
||||
- Focus on core planning fields
|
||||
- Read-only access to legacy data
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Admin Interface
|
||||
- ✅ Tasks admin loads without errors
|
||||
- ✅ Content admin shows new fields
|
||||
- ✅ ContentTaxonomy admin registered
|
||||
- ✅ ContentAttribute admin registered
|
||||
- ✅ ContentIdeas admin updated
|
||||
- ✅ All deprecated fields marked read-only
|
||||
- ✅ Fieldsets organized properly
|
||||
|
||||
### API Endpoints
|
||||
- ✅ `/api/v1/writer/taxonomies/` accessible
|
||||
- ✅ `/api/v1/writer/attributes/` accessible
|
||||
- ✅ Content filters work with new fields
|
||||
- ✅ ContentIdeas filters updated
|
||||
- ✅ No 500 errors on backend restart
|
||||
|
||||
### Database
|
||||
- ✅ All migrations applied
|
||||
- ✅ New tables exist
|
||||
- ✅ New fields in Content table
|
||||
- ✅ M2M relationships functional
|
||||
|
||||
---
|
||||
|
||||
## 📚 Usage Examples
|
||||
|
||||
### Create Taxonomy via API
|
||||
```bash
|
||||
POST /api/v1/writer/taxonomies/
|
||||
{
|
||||
"name": "SEO",
|
||||
"slug": "seo",
|
||||
"taxonomy_type": "category",
|
||||
"description": "All about SEO",
|
||||
"site_id": 5,
|
||||
"sector_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Create Product Attribute via API
|
||||
```bash
|
||||
POST /api/v1/writer/attributes/
|
||||
{
|
||||
"name": "Color",
|
||||
"value": "Blue",
|
||||
"attribute_type": "product_spec",
|
||||
"content": 42,
|
||||
"external_id": 101,
|
||||
"external_attribute_name": "pa_color",
|
||||
"source": "wordpress",
|
||||
"site_id": 5,
|
||||
"sector_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Filter Content by New Fields
|
||||
```bash
|
||||
GET /api/v1/writer/content/?entity_type=post&content_format=listicle&cluster_role=hub
|
||||
GET /api/v1/writer/content/?source=wordpress&sync_status=imported
|
||||
GET /api/v1/writer/taxonomies/?taxonomy_type=category&sync_status=imported
|
||||
GET /api/v1/writer/attributes/?attribute_type=product_spec&source=wordpress
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
### Ready for Frontend Integration
|
||||
|
||||
1. **Site Settings → Content Types Tab**
|
||||
- Display taxonomies from `/api/v1/writer/taxonomies/`
|
||||
- Show attributes from `/api/v1/writer/attributes/`
|
||||
- Enable/disable sync per type
|
||||
- Set fetch limits
|
||||
|
||||
2. **Content Management**
|
||||
- Filter content by `entity_type`, `content_format`, `cluster_role`
|
||||
- Display WordPress sync status
|
||||
- Show assigned taxonomies
|
||||
- View product attributes
|
||||
|
||||
3. **WordPress Import UI**
|
||||
- Fetch structure from plugin
|
||||
- Create ContentTaxonomy records
|
||||
- Import content titles
|
||||
- Map to clusters
|
||||
|
||||
---
|
||||
|
||||
## ✅ Summary
|
||||
|
||||
**All admin interfaces and API views updated to use unified content architecture.**
|
||||
|
||||
**Changes:**
|
||||
- ✅ 3 new admin classes registered
|
||||
- ✅ 2 new ViewSets added
|
||||
- ✅ 7 new filter fields in Content
|
||||
- ✅ 5 new filter fields in Taxonomies
|
||||
- ✅ 5 new filter fields in Attributes
|
||||
- ✅ All deprecated fields marked read-only
|
||||
- ✅ Backward compatibility maintained
|
||||
- ✅ Backend restart successful
|
||||
- ✅ No linter errors
|
||||
|
||||
**New Endpoints:**
|
||||
- `/api/v1/writer/taxonomies/` (full CRUD + custom actions)
|
||||
- `/api/v1/writer/attributes/` (full CRUD)
|
||||
|
||||
**Status:** Production-ready, fully functional, WordPress integration prepared.
|
||||
|
||||
@@ -1,482 +0,0 @@
|
||||
# ✅ Cleanup Complete - Unified Content Architecture
|
||||
|
||||
**Date**: November 22, 2025
|
||||
**Status**: ✅ **COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully cleaned up all redundant and deprecated fields from the IGNY8 backend, migrated data to the new unified content architecture, and created a Sites content types interface endpoint.
|
||||
|
||||
---
|
||||
|
||||
## What Was Completed
|
||||
|
||||
### 1. ✅ Removed Deprecated Fields from Models
|
||||
|
||||
**ContentIdeas Model** (`/backend/igny8_core/business/planning/models.py`):
|
||||
- ❌ Removed: `content_structure` (replaced by `cluster_role`)
|
||||
- ❌ Removed: `content_type` (replaced by `site_entity_type`)
|
||||
- ✅ Kept: `site_entity_type` (post, page, product, service, taxonomy_term)
|
||||
- ✅ Kept: `cluster_role` (hub, supporting, attribute)
|
||||
|
||||
**Tasks Model** (`/backend/igny8_core/business/content/models.py`):
|
||||
- ❌ Removed: `content_structure` (replaced by `cluster_role`)
|
||||
- ❌ Removed: `content_type` (replaced by `entity_type`)
|
||||
- ❌ Removed: `content` (moved to Content model)
|
||||
- ❌ Removed: `word_count` (moved to Content model)
|
||||
- ❌ Removed: `meta_title` (moved to Content model)
|
||||
- ❌ Removed: `meta_description` (moved to Content model)
|
||||
- ❌ Removed: `assigned_post_id` (moved to Content model)
|
||||
- ❌ Removed: `post_url` (moved to Content model)
|
||||
- ✅ Kept: `entity_type` (post, page, product, service, taxonomy_term)
|
||||
- ✅ Kept: `cluster_role` (hub, supporting, attribute)
|
||||
|
||||
**Content Model** (`/backend/igny8_core/business/content/models.py`):
|
||||
- ❌ Removed: `categories` (JSON field, replaced by `taxonomies` M2M)
|
||||
- ❌ Removed: `tags` (JSON field, replaced by `taxonomies` M2M)
|
||||
- ✅ Kept: `entity_type` (post, page, product, service, taxonomy_term)
|
||||
- ✅ Kept: `content_format` (article, listicle, guide, comparison, review, roundup)
|
||||
- ✅ Kept: `cluster_role` (hub, supporting, attribute)
|
||||
- ✅ Kept: `taxonomies` (M2M to ContentTaxonomy)
|
||||
|
||||
---
|
||||
|
||||
### 2. ✅ Updated Admin Interfaces
|
||||
|
||||
**ContentIdeas Admin** (`/backend/igny8_core/modules/planner/admin.py`):
|
||||
- Removed deprecated fields from `readonly_fields`
|
||||
- Removed "Deprecated Fields" fieldset
|
||||
- Updated `list_display` to show only new fields
|
||||
- Updated `list_filter` to use only new fields
|
||||
|
||||
**Tasks Admin** (`/backend/igny8_core/modules/writer/admin.py`):
|
||||
- Added `entity_type` and `cluster_role` to `list_display`
|
||||
- Added `entity_type` and `cluster_role` to `list_filter`
|
||||
- Removed deprecated fields from fieldsets
|
||||
- Added "Content Classification" fieldset with new fields
|
||||
|
||||
**Content Admin** (`/backend/igny8_core/modules/writer/admin.py`):
|
||||
- Removed deprecated `categories` and `tags` from `readonly_fields`
|
||||
- Removed "Deprecated Fields" fieldset
|
||||
- All new fields properly displayed and filterable
|
||||
|
||||
---
|
||||
|
||||
### 3. ✅ Updated API Views
|
||||
|
||||
**ContentIdeasViewSet** (`/backend/igny8_core/modules/planner/views.py`):
|
||||
- `filterset_fields`: Uses `site_entity_type` and `cluster_role` (no deprecated fields)
|
||||
|
||||
**TasksViewSet** (`/backend/igny8_core/modules/writer/views.py`):
|
||||
- `filterset_fields`: Added `entity_type`, `cluster_role`
|
||||
- `ordering_fields`: Removed `word_count` (no longer in model)
|
||||
|
||||
**ContentViewSet** (`/backend/igny8_core/modules/writer/views.py`):
|
||||
- Already updated with all new fields
|
||||
- Filters working correctly
|
||||
|
||||
---
|
||||
|
||||
### 4. ✅ Data Migration
|
||||
|
||||
**Migration**: `0006_cleanup_migrate_and_drop_deprecated_fields.py`
|
||||
|
||||
**Data Migration Logic**:
|
||||
- Ensured all `Tasks` have default `entity_type` ('post') and `cluster_role` ('hub')
|
||||
- Ensured all `Content` inherit `entity_type` and `cluster_role` from their related `Task`
|
||||
- Set defaults for any `Content` without a task
|
||||
|
||||
**Database Changes**:
|
||||
- Dropped `content_structure` column from `igny8_content_ideas`
|
||||
- Dropped `content_type` column from `igny8_content_ideas`
|
||||
- Dropped `content_structure` column from `igny8_tasks`
|
||||
- Dropped `content_type` column from `igny8_tasks`
|
||||
- Dropped `content` column from `igny8_tasks`
|
||||
- Dropped `word_count` column from `igny8_tasks`
|
||||
- Dropped `meta_title` column from `igny8_tasks`
|
||||
- Dropped `meta_description` column from `igny8_tasks`
|
||||
- Dropped `assigned_post_id` column from `igny8_tasks`
|
||||
- Dropped `post_url` column from `igny8_tasks`
|
||||
- Dropped `categories` column from `igny8_content`
|
||||
- Dropped `tags` column from `igny8_content`
|
||||
|
||||
---
|
||||
|
||||
### 5. ✅ Created Sites Content Types Interface
|
||||
|
||||
**New Endpoint**: `GET /api/v1/integration/integrations/{id}/content-types/`
|
||||
|
||||
**Purpose**: Show WordPress synced content types with counts
|
||||
|
||||
**Response Format**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"post_types": {
|
||||
"post": {
|
||||
"label": "Posts",
|
||||
"count": 123,
|
||||
"synced_count": 50,
|
||||
"enabled": true,
|
||||
"fetch_limit": 100,
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"page": {
|
||||
"label": "Pages",
|
||||
"count": 12,
|
||||
"synced_count": 12,
|
||||
"enabled": true,
|
||||
"fetch_limit": 50,
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"product": {
|
||||
"label": "Products",
|
||||
"count": 456,
|
||||
"synced_count": 200,
|
||||
"enabled": true,
|
||||
"fetch_limit": 200,
|
||||
"last_synced": null
|
||||
}
|
||||
},
|
||||
"taxonomies": {
|
||||
"category": {
|
||||
"label": "Categories",
|
||||
"count": 25,
|
||||
"synced_count": 25,
|
||||
"enabled": true,
|
||||
"fetch_limit": 100,
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"post_tag": {
|
||||
"label": "Tags",
|
||||
"count": 102,
|
||||
"synced_count": 80,
|
||||
"enabled": true,
|
||||
"fetch_limit": 200,
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"product_cat": {
|
||||
"label": "Product Categories",
|
||||
"count": 15,
|
||||
"synced_count": 15,
|
||||
"enabled": false,
|
||||
"fetch_limit": 50,
|
||||
"last_synced": null
|
||||
}
|
||||
},
|
||||
"last_structure_fetch": "2025-11-22T10:00:00Z",
|
||||
"plugin_connection_enabled": true,
|
||||
"two_way_sync_enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Features**:
|
||||
- Shows WP content type counts from plugin
|
||||
- Shows synced counts from IGNY8 database
|
||||
- Shows enabled/disabled status
|
||||
- Shows fetch limits
|
||||
- Shows last sync timestamps
|
||||
|
||||
---
|
||||
|
||||
## Unified Field Structure
|
||||
|
||||
### Entity Type (Standardized)
|
||||
|
||||
**Field**: `entity_type`
|
||||
**Used In**: ContentIdeas (`site_entity_type`), Tasks, Content
|
||||
|
||||
**Values**:
|
||||
- `post` - Blog posts, articles
|
||||
- `page` - Static pages
|
||||
- `product` - WooCommerce products
|
||||
- `service` - Service pages
|
||||
- `taxonomy_term` - Category/tag pages
|
||||
|
||||
### Content Format (For Posts Only)
|
||||
|
||||
**Field**: `content_format`
|
||||
**Used In**: Content
|
||||
|
||||
**Values**:
|
||||
- `article` - Standard article
|
||||
- `listicle` - List-based content
|
||||
- `guide` - How-to guide
|
||||
- `comparison` - Comparison article
|
||||
- `review` - Product/service review
|
||||
- `roundup` - Roundup/collection
|
||||
|
||||
### Cluster Role
|
||||
|
||||
**Field**: `cluster_role`
|
||||
**Used In**: ContentIdeas, Tasks, Content
|
||||
|
||||
**Values**:
|
||||
- `hub` - Main cluster page
|
||||
- `supporting` - Supporting content
|
||||
- `attribute` - Attribute-focused page
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (Final)
|
||||
|
||||
### igny8_content_ideas
|
||||
```sql
|
||||
- id
|
||||
- idea_title
|
||||
- description
|
||||
- site_entity_type ✅ NEW (replaces content_structure + content_type)
|
||||
- cluster_role ✅ NEW (replaces content_structure)
|
||||
- keyword_cluster_id
|
||||
- taxonomy_id
|
||||
- status
|
||||
- estimated_word_count
|
||||
- site_id, sector_id, account_id
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
### igny8_tasks
|
||||
```sql
|
||||
- id
|
||||
- title
|
||||
- description
|
||||
- keywords
|
||||
- entity_type ✅ NEW (replaces content_type)
|
||||
- cluster_role ✅ NEW (replaces content_structure)
|
||||
- cluster_id
|
||||
- idea_id
|
||||
- taxonomy_id
|
||||
- status
|
||||
- site_id, sector_id, account_id
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
### igny8_content
|
||||
```sql
|
||||
- id
|
||||
- task_id
|
||||
- cluster_id
|
||||
- title
|
||||
- html_content
|
||||
- word_count
|
||||
- entity_type ✅ NEW
|
||||
- content_format ✅ NEW
|
||||
- cluster_role ✅ NEW
|
||||
- external_type (WP post type)
|
||||
- external_id, external_url
|
||||
- source, sync_status
|
||||
- meta_title, meta_description
|
||||
- primary_keyword, secondary_keywords
|
||||
- taxonomies (M2M via ContentTaxonomyRelation) ✅ NEW
|
||||
- site_id, sector_id, account_id
|
||||
- generated_at, updated_at
|
||||
```
|
||||
|
||||
### igny8_content_taxonomies ✅ NEW
|
||||
```sql
|
||||
- id
|
||||
- name, slug
|
||||
- taxonomy_type (category, tag, product_cat, product_tag, product_attr)
|
||||
- parent_id
|
||||
- external_id, external_taxonomy
|
||||
- sync_status
|
||||
- count, description
|
||||
- metadata
|
||||
- site_id, sector_id, account_id
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
### igny8_content_attributes ✅ NEW
|
||||
```sql
|
||||
- id
|
||||
- content_id, task_id, cluster_id
|
||||
- attribute_type (product_spec, service_modifier, semantic_facet)
|
||||
- name, value
|
||||
- source (blueprint, manual, import, wordpress)
|
||||
- metadata
|
||||
- external_id, external_attribute_name
|
||||
- site_id, sector_id, account_id
|
||||
- created_at, updated_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints (Updated)
|
||||
|
||||
### Planner Module
|
||||
|
||||
**ContentIdeas**:
|
||||
- `GET /api/v1/planner/ideas/` - List (filters: `status`, `site_entity_type`, `cluster_role`)
|
||||
- `POST /api/v1/planner/ideas/` - Create
|
||||
- `GET /api/v1/planner/ideas/{id}/` - Retrieve
|
||||
- `PATCH /api/v1/planner/ideas/{id}/` - Update
|
||||
- `DELETE /api/v1/planner/ideas/{id}/` - Delete
|
||||
|
||||
### Writer Module
|
||||
|
||||
**Tasks**:
|
||||
- `GET /api/v1/writer/tasks/` - List (filters: `status`, `entity_type`, `cluster_role`, `cluster_id`)
|
||||
- `POST /api/v1/writer/tasks/` - Create
|
||||
- `GET /api/v1/writer/tasks/{id}/` - Retrieve
|
||||
- `PATCH /api/v1/writer/tasks/{id}/` - Update
|
||||
- `DELETE /api/v1/writer/tasks/{id}/` - Delete
|
||||
|
||||
**Content**:
|
||||
- `GET /api/v1/writer/content/` - List (filters: `entity_type`, `content_format`, `cluster_role`, `source`, `sync_status`, `external_type`)
|
||||
- `POST /api/v1/writer/content/` - Create
|
||||
- `GET /api/v1/writer/content/{id}/` - Retrieve
|
||||
- `PATCH /api/v1/writer/content/{id}/` - Update
|
||||
- `DELETE /api/v1/writer/content/{id}/` - Delete
|
||||
|
||||
**ContentTaxonomy** ✅ NEW:
|
||||
- `GET /api/v1/writer/taxonomies/` - List
|
||||
- `POST /api/v1/writer/taxonomies/` - Create
|
||||
- `GET /api/v1/writer/taxonomies/{id}/` - Retrieve
|
||||
- `PATCH /api/v1/writer/taxonomies/{id}/` - Update
|
||||
- `DELETE /api/v1/writer/taxonomies/{id}/` - Delete
|
||||
|
||||
**ContentAttribute** ✅ NEW:
|
||||
- `GET /api/v1/writer/attributes/` - List
|
||||
- `POST /api/v1/writer/attributes/` - Create
|
||||
- `GET /api/v1/writer/attributes/{id}/` - Retrieve
|
||||
- `PATCH /api/v1/writer/attributes/{id}/` - Update
|
||||
- `DELETE /api/v1/writer/attributes/{id}/` - Delete
|
||||
|
||||
### Integration Module ✅ NEW
|
||||
|
||||
**Content Types Summary**:
|
||||
- `GET /api/v1/integration/integrations/{id}/content-types/` - Get synced content types with counts
|
||||
|
||||
---
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Sites Settings - Content Types Tab
|
||||
|
||||
**URL**: `/sites/{site_id}/settings` → "Content Types" tab
|
||||
|
||||
**API Call**:
|
||||
```javascript
|
||||
// Get integration for site
|
||||
const integration = await api.get(`/integration/integrations/?site_id=${siteId}&platform=wordpress`);
|
||||
|
||||
// Get content types summary
|
||||
const summary = await api.get(`/integration/integrations/${integration.id}/content-types/`);
|
||||
```
|
||||
|
||||
**Display**:
|
||||
1. **Post Types Section**
|
||||
- Show each post type with label, count, synced count
|
||||
- Enable/disable toggle
|
||||
- Fetch limit input
|
||||
- Last synced timestamp
|
||||
- Sync button
|
||||
|
||||
2. **Taxonomies Section**
|
||||
- Show each taxonomy with label, count, synced count
|
||||
- Enable/disable toggle
|
||||
- Fetch limit input
|
||||
- Last synced timestamp
|
||||
- Sync button
|
||||
|
||||
3. **Actions**
|
||||
- "Fetch Structure" button - Refresh from WordPress
|
||||
- "Sync All" button - Import enabled types
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### ✅ Backend Tests
|
||||
|
||||
- [x] Migrations applied successfully
|
||||
- [x] No deprecated fields in models
|
||||
- [x] Admin interfaces show only new fields
|
||||
- [x] API endpoints return correct data
|
||||
- [x] Filters work with new fields
|
||||
- [x] Content types endpoint returns data
|
||||
- [x] Backend restarted successfully
|
||||
|
||||
### ⏳ Frontend Tests (Pending)
|
||||
|
||||
- [ ] Sites settings page loads
|
||||
- [ ] Content Types tab visible
|
||||
- [ ] Content types summary displays
|
||||
- [ ] Enable/disable toggles work
|
||||
- [ ] Fetch limit inputs work
|
||||
- [ ] Sync buttons trigger API calls
|
||||
- [ ] Counts update after sync
|
||||
|
||||
---
|
||||
|
||||
## Migration Timeline
|
||||
|
||||
| Phase | Description | Status |
|
||||
|-------|-------------|--------|
|
||||
| Phase 1 | Add new models and fields | ✅ Complete |
|
||||
| Phase 2 | Migrate data to new structure | ✅ Complete |
|
||||
| Phase 3 | Mark deprecated fields | ✅ Complete |
|
||||
| Phase 4 | Update admin interfaces | ✅ Complete |
|
||||
| Phase 5 | Update API views | ✅ Complete |
|
||||
| Phase 6 | Migrate data and drop columns | ✅ Complete |
|
||||
| Phase 7 | Create Sites interface endpoint | ✅ Complete |
|
||||
| Phase 8 | Build frontend UI | ⏳ Pending |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate (Backend Complete ✅)
|
||||
1. ✅ All deprecated fields removed
|
||||
2. ✅ All admin interfaces updated
|
||||
3. ✅ All API endpoints updated
|
||||
4. ✅ Data migrated successfully
|
||||
5. ✅ Sites content types endpoint created
|
||||
|
||||
### Soon (Frontend)
|
||||
1. Create "Content Types" tab in Sites Settings
|
||||
2. Display content types summary
|
||||
3. Add enable/disable toggles
|
||||
4. Add fetch limit inputs
|
||||
5. Add sync buttons
|
||||
6. Test end-to-end workflow
|
||||
|
||||
### Later (Advanced Features)
|
||||
1. Implement `IntegrationService.fetch_content_structure()`
|
||||
2. Implement `IntegrationService.import_taxonomies()`
|
||||
3. Implement `IntegrationService.import_content_titles()`
|
||||
4. Add AI semantic mapping for clusters
|
||||
5. Add bulk content optimization
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Status**: ✅ **BACKEND CLEANUP COMPLETE**
|
||||
|
||||
All redundant and deprecated fields have been removed from the backend. The unified content architecture is now fully implemented and operational. The Sites content types interface endpoint is ready for frontend integration.
|
||||
|
||||
**What Changed**:
|
||||
- ❌ Removed 14 deprecated fields across 3 models
|
||||
- ✅ Standardized on `entity_type`, `content_format`, `cluster_role`
|
||||
- ✅ Replaced JSON fields with proper M2M relationships
|
||||
- ✅ Updated all admin interfaces
|
||||
- ✅ Updated all API endpoints
|
||||
- ✅ Created Sites content types summary endpoint
|
||||
|
||||
**Result**: Clean, standardized, production-ready content architecture with WordPress integration support.
|
||||
|
||||
---
|
||||
|
||||
**Completion Time**: ~2 hours
|
||||
**Files Modified**: 12
|
||||
**Migrations Created**: 2
|
||||
**Database Columns Dropped**: 14
|
||||
**New API Endpoints**: 1
|
||||
|
||||
✅ **READY FOR FRONTEND INTEGRATION**
|
||||
|
||||
@@ -1,394 +0,0 @@
|
||||
# ✅ Complete Update Checklist - All Verified
|
||||
|
||||
**Date**: November 21, 2025
|
||||
**Status**: ✅ **ALL COMPLETE & VERIFIED**
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 1: Database Migrations
|
||||
|
||||
### Migrations Applied
|
||||
```
|
||||
writer
|
||||
✅ 0001_initial
|
||||
✅ 0002_phase1_add_unified_taxonomy_and_attributes
|
||||
✅ 0003_phase1b_fix_taxonomy_relation
|
||||
✅ 0004_phase2_migrate_data_to_unified_structure
|
||||
✅ 0005_phase3_mark_deprecated_fields
|
||||
|
||||
planner
|
||||
✅ 0001_initial
|
||||
✅ 0002_initial
|
||||
```
|
||||
|
||||
### New Tables Created
|
||||
```sql
|
||||
✅ igny8_content_taxonomy_terms (16 columns, 23 indexes)
|
||||
✅ igny8_content_attributes (16 columns, 15 indexes)
|
||||
✅ igny8_content_taxonomy_relations (4 columns, 3 indexes)
|
||||
✅ igny8_content_taxonomy_terms_clusters (M2M table)
|
||||
```
|
||||
|
||||
### New Fields in Content Table
|
||||
```sql
|
||||
✅ cluster_id (bigint)
|
||||
✅ cluster_role (varchar)
|
||||
✅ content_format (varchar)
|
||||
✅ external_type (varchar)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 2: Models Updated
|
||||
|
||||
### Writer Module (`igny8_core/business/content/models.py`)
|
||||
|
||||
#### Content Model
|
||||
- ✅ Added `content_format` field (article, listicle, guide, comparison, review, roundup)
|
||||
- ✅ Added `cluster_role` field (hub, supporting, attribute)
|
||||
- ✅ Added `external_type` field (WP post type)
|
||||
- ✅ Added `cluster` FK (direct cluster relationship)
|
||||
- ✅ Added `taxonomies` M2M (via ContentTaxonomyRelation)
|
||||
- ✅ Updated `entity_type` choices (post, page, product, service, taxonomy_term)
|
||||
- ✅ Marked `categories` and `tags` as deprecated
|
||||
|
||||
#### ContentTaxonomy Model (NEW)
|
||||
- ✅ Unified taxonomy model created
|
||||
- ✅ Supports categories, tags, product attributes
|
||||
- ✅ WordPress sync fields (external_id, external_taxonomy, sync_status)
|
||||
- ✅ Hierarchical support (parent FK)
|
||||
- ✅ Cluster mapping (M2M to Clusters)
|
||||
- ✅ 23 indexes for performance
|
||||
|
||||
#### ContentAttribute Model (NEW)
|
||||
- ✅ Enhanced from ContentAttributeMap
|
||||
- ✅ Added attribute_type (product_spec, service_modifier, semantic_facet)
|
||||
- ✅ Added WP sync fields (external_id, external_attribute_name)
|
||||
- ✅ Added cluster FK for semantic attributes
|
||||
- ✅ 15 indexes for performance
|
||||
|
||||
#### Tasks Model
|
||||
- ✅ Marked 10 fields as deprecated (help_text updated)
|
||||
- ✅ Fields preserved for backward compatibility
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 3: Admin Interfaces Updated
|
||||
|
||||
### Writer Admin (`igny8_core/modules/writer/admin.py`)
|
||||
|
||||
#### TasksAdmin
|
||||
- ✅ Simplified list_display (removed deprecated fields)
|
||||
- ✅ Updated list_filter (removed content_type, content_structure)
|
||||
- ✅ Added fieldsets with "Deprecated Fields" section (collapsed)
|
||||
- ✅ Marked 6 fields as readonly
|
||||
|
||||
#### ContentAdmin
|
||||
- ✅ Added entity_type, content_format, cluster_role to list_display
|
||||
- ✅ Added source, sync_status to list_filter
|
||||
- ✅ Created 7 organized fieldsets
|
||||
- ✅ Removed filter_horizontal for taxonomies (through model issue)
|
||||
- ✅ Marked categories, tags as readonly
|
||||
|
||||
#### ContentTaxonomyAdmin (NEW)
|
||||
- ✅ Full CRUD interface
|
||||
- ✅ List display with all key fields
|
||||
- ✅ Filters: taxonomy_type, sync_status, parent
|
||||
- ✅ Search: name, slug, description
|
||||
- ✅ filter_horizontal for clusters M2M
|
||||
- ✅ 4 organized fieldsets
|
||||
|
||||
#### ContentAttributeAdmin (NEW)
|
||||
- ✅ Full CRUD interface
|
||||
- ✅ List display with all key fields
|
||||
- ✅ Filters: attribute_type, source
|
||||
- ✅ Search: name, value, external_attribute_name
|
||||
- ✅ 3 organized fieldsets
|
||||
|
||||
### Planner Admin (`igny8_core/modules/planner/admin.py`)
|
||||
|
||||
#### ContentIdeasAdmin
|
||||
- ✅ Replaced content_structure, content_type with site_entity_type, cluster_role
|
||||
- ✅ Updated list_display
|
||||
- ✅ Updated list_filter
|
||||
- ✅ Added fieldsets with deprecated fields section
|
||||
- ✅ Marked old fields as readonly
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 4: API Views & Serializers Updated
|
||||
|
||||
### Writer Views (`igny8_core/modules/writer/views.py`)
|
||||
|
||||
#### TasksViewSet
|
||||
- ✅ Removed deprecated filters (content_type, content_structure)
|
||||
- ✅ Simplified filterset_fields to ['status', 'cluster_id']
|
||||
|
||||
#### ContentViewSet
|
||||
- ✅ Optimized queryset (select_related, prefetch_related)
|
||||
- ✅ Added 5 new filters: entity_type, content_format, cluster_role, source, sync_status
|
||||
- ✅ Added external_type filter
|
||||
- ✅ Added external_url to search_fields
|
||||
- ✅ Updated ordering_fields
|
||||
|
||||
#### ContentTaxonomyViewSet (NEW)
|
||||
- ✅ Full CRUD endpoints
|
||||
- ✅ Filters: taxonomy_type, sync_status, parent, external_id, external_taxonomy
|
||||
- ✅ Search: name, slug, description
|
||||
- ✅ Custom action: map_to_cluster
|
||||
- ✅ Custom action: contents (get all content for taxonomy)
|
||||
- ✅ Optimized queryset
|
||||
|
||||
#### ContentAttributeViewSet (NEW)
|
||||
- ✅ Full CRUD endpoints
|
||||
- ✅ Filters: attribute_type, source, content, cluster, external_id
|
||||
- ✅ Search: name, value, external_attribute_name
|
||||
- ✅ Optimized queryset
|
||||
|
||||
### Writer Serializers (`igny8_core/modules/writer/serializers.py`)
|
||||
|
||||
#### ContentTaxonomySerializer (NEW)
|
||||
- ✅ All fields exposed
|
||||
- ✅ parent_name computed field
|
||||
- ✅ cluster_names computed field
|
||||
- ✅ content_count computed field
|
||||
|
||||
#### ContentAttributeSerializer (NEW)
|
||||
- ✅ All fields exposed
|
||||
- ✅ content_title computed field
|
||||
- ✅ cluster_name computed field
|
||||
|
||||
#### ContentTaxonomyRelationSerializer (NEW)
|
||||
- ✅ Through model serializer
|
||||
- ✅ content_title, taxonomy_name, taxonomy_type computed fields
|
||||
|
||||
### Planner Views (`igny8_core/modules/planner/views.py`)
|
||||
|
||||
#### ContentIdeasViewSet
|
||||
- ✅ Updated filterset_fields: replaced content_structure, content_type with site_entity_type, cluster_role
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 5: URL Routes Updated
|
||||
|
||||
### Writer URLs (`igny8_core/modules/writer/urls.py`)
|
||||
- ✅ Added taxonomies route: `/api/v1/writer/taxonomies/`
|
||||
- ✅ Added attributes route: `/api/v1/writer/attributes/`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Phase 6: Backend Status
|
||||
|
||||
### Server
|
||||
- ✅ Backend restarted successfully
|
||||
- ✅ 4 gunicorn workers running
|
||||
- ✅ No errors in logs
|
||||
- ✅ No linter errors
|
||||
|
||||
### Database
|
||||
- ✅ All migrations applied
|
||||
- ✅ New tables verified
|
||||
- ✅ New fields verified
|
||||
- ✅ M2M relationships functional
|
||||
|
||||
---
|
||||
|
||||
## 📊 Complete Feature Matrix
|
||||
|
||||
### Content Management
|
||||
|
||||
| Feature | Old | New | Status |
|
||||
|---------|-----|-----|--------|
|
||||
| Entity Type | Multiple overlapping fields | Single `entity_type` + `content_format` | ✅ |
|
||||
| Categories/Tags | JSON arrays | M2M ContentTaxonomy | ✅ |
|
||||
| Attributes | ContentAttributeMap | Enhanced ContentAttribute | ✅ |
|
||||
| WP Sync | No support | Full sync fields | ✅ |
|
||||
| Cluster Mapping | Via mapping table | Direct FK + M2M | ✅ |
|
||||
|
||||
### Admin Interfaces
|
||||
|
||||
| Model | List Display | Filters | Fieldsets | Status |
|
||||
|-------|-------------|---------|-----------|--------|
|
||||
| Tasks | Updated | Simplified | 3 sections | ✅ |
|
||||
| Content | Enhanced | 9 filters | 7 sections | ✅ |
|
||||
| ContentTaxonomy | NEW | 5 filters | 4 sections | ✅ |
|
||||
| ContentAttribute | NEW | 4 filters | 3 sections | ✅ |
|
||||
| ContentIdeas | Updated | Updated | 4 sections | ✅ |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Methods | Filters | Custom Actions | Status |
|
||||
|----------|---------|---------|----------------|--------|
|
||||
| /writer/tasks/ | CRUD | 2 filters | Multiple | ✅ |
|
||||
| /writer/content/ | CRUD | 9 filters | Multiple | ✅ |
|
||||
| /writer/taxonomies/ | CRUD | 5 filters | 2 actions | ✅ NEW |
|
||||
| /writer/attributes/ | CRUD | 5 filters | - | ✅ NEW |
|
||||
| /planner/ideas/ | CRUD | 4 filters | Multiple | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verification Tests
|
||||
|
||||
### Database Tests
|
||||
```bash
|
||||
✅ SELECT COUNT(*) FROM igny8_content_taxonomy_terms;
|
||||
✅ SELECT COUNT(*) FROM igny8_content_attributes;
|
||||
✅ SELECT COUNT(*) FROM igny8_content_taxonomy_relations;
|
||||
✅ \d igny8_content (verify new columns exist)
|
||||
```
|
||||
|
||||
### Admin Tests
|
||||
```bash
|
||||
✅ Access /admin/writer/tasks/ - loads without errors
|
||||
✅ Access /admin/writer/content/ - shows new filters
|
||||
✅ Access /admin/writer/contenttaxonomy/ - NEW admin works
|
||||
✅ Access /admin/writer/contentattribute/ - NEW admin works
|
||||
✅ Access /admin/planner/contentideas/ - updated fields visible
|
||||
```
|
||||
|
||||
### API Tests
|
||||
```bash
|
||||
✅ GET /api/v1/writer/tasks/ - returns data
|
||||
✅ GET /api/v1/writer/content/?entity_type=post - filters work
|
||||
✅ GET /api/v1/writer/taxonomies/ - NEW endpoint accessible
|
||||
✅ GET /api/v1/writer/attributes/ - NEW endpoint accessible
|
||||
✅ GET /api/v1/planner/ideas/?site_entity_type=post - filters work
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Updated Files Summary
|
||||
|
||||
### Models
|
||||
- ✅ `igny8_core/business/content/models.py` (3 new models, enhanced Content)
|
||||
|
||||
### Admin
|
||||
- ✅ `igny8_core/modules/writer/admin.py` (4 admin classes updated/added)
|
||||
- ✅ `igny8_core/modules/planner/admin.py` (1 admin class updated)
|
||||
|
||||
### Views
|
||||
- ✅ `igny8_core/modules/writer/views.py` (4 ViewSets updated/added)
|
||||
- ✅ `igny8_core/modules/planner/views.py` (1 ViewSet updated)
|
||||
|
||||
### Serializers
|
||||
- ✅ `igny8_core/modules/writer/serializers.py` (3 new serializers added)
|
||||
|
||||
### URLs
|
||||
- ✅ `igny8_core/modules/writer/urls.py` (2 new routes added)
|
||||
|
||||
### Migrations
|
||||
- ✅ 5 new migration files created and applied
|
||||
|
||||
---
|
||||
|
||||
## 🎯 What's Now Available
|
||||
|
||||
### For Developers
|
||||
1. ✅ Unified content entity system (entity_type + content_format)
|
||||
2. ✅ Real taxonomy relationships (not JSON)
|
||||
3. ✅ Enhanced attribute system with WP sync
|
||||
4. ✅ Direct cluster relationships
|
||||
5. ✅ Full CRUD APIs for all new models
|
||||
6. ✅ Comprehensive admin interfaces
|
||||
|
||||
### For WordPress Integration
|
||||
1. ✅ ContentTaxonomy model ready for WP terms
|
||||
2. ✅ ContentAttribute model ready for WooCommerce attributes
|
||||
3. ✅ Content model has all WP sync fields
|
||||
4. ✅ API endpoints ready for import/sync
|
||||
5. ✅ Semantic cluster mapping ready
|
||||
|
||||
### For Frontend
|
||||
1. ✅ New filter options for content (entity_type, content_format, cluster_role)
|
||||
2. ✅ Taxonomy management endpoints
|
||||
3. ✅ Attribute management endpoints
|
||||
4. ✅ WordPress sync status tracking
|
||||
5. ✅ Cluster mapping capabilities
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation Created
|
||||
|
||||
1. ✅ `/data/app/igny8/backend/MIGRATION_SUMMARY.md`
|
||||
- Complete database migration details
|
||||
- Phase 1, 2, 3 breakdown
|
||||
- Rollback instructions
|
||||
|
||||
2. ✅ `/data/app/igny8/backend/NEW_ARCHITECTURE_GUIDE.md`
|
||||
- Quick reference guide
|
||||
- Usage examples
|
||||
- Query patterns
|
||||
- WordPress sync workflows
|
||||
|
||||
3. ✅ `/data/app/igny8/backend/ADMIN_VIEWS_UPDATE_SUMMARY.md`
|
||||
- Admin interface changes
|
||||
- API endpoint details
|
||||
- Filter documentation
|
||||
- Testing checklist
|
||||
|
||||
4. ✅ `/data/app/igny8/backend/COMPLETE_UPDATE_CHECKLIST.md` (this file)
|
||||
- Comprehensive verification
|
||||
- All changes documented
|
||||
- Status tracking
|
||||
|
||||
---
|
||||
|
||||
## ✅ Final Status
|
||||
|
||||
### All Tasks Complete
|
||||
|
||||
| Task | Status |
|
||||
|------|--------|
|
||||
| Database migrations | ✅ COMPLETE |
|
||||
| Model updates | ✅ COMPLETE |
|
||||
| Admin interfaces | ✅ COMPLETE |
|
||||
| API views | ✅ COMPLETE |
|
||||
| Serializers | ✅ COMPLETE |
|
||||
| URL routes | ✅ COMPLETE |
|
||||
| Filters updated | ✅ COMPLETE |
|
||||
| Forms updated | ✅ COMPLETE |
|
||||
| Backend restart | ✅ SUCCESS |
|
||||
| Documentation | ✅ COMPLETE |
|
||||
|
||||
### Zero Issues
|
||||
- ✅ No migration errors
|
||||
- ✅ No linter errors
|
||||
- ✅ No admin errors
|
||||
- ✅ No API errors
|
||||
- ✅ No startup errors
|
||||
|
||||
### Production Ready
|
||||
- ✅ Backward compatible
|
||||
- ✅ Non-breaking changes
|
||||
- ✅ Deprecated fields preserved
|
||||
- ✅ All tests passing
|
||||
- ✅ Documentation complete
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps (When Ready)
|
||||
|
||||
### Phase 4: WordPress Integration Implementation
|
||||
1. Backend service methods for WP import
|
||||
2. Frontend "Content Types" tab in Site Settings
|
||||
3. AI semantic mapping endpoint
|
||||
4. Sync status tracking UI
|
||||
5. Bulk import workflows
|
||||
|
||||
### Phase 5: Blueprint Cleanup (Optional)
|
||||
1. Migrate remaining blueprint data
|
||||
2. Drop deprecated blueprint tables
|
||||
3. Remove deprecated fields from models
|
||||
4. Final cleanup migration
|
||||
|
||||
---
|
||||
|
||||
**✅ ALL MIGRATIONS RUN**
|
||||
**✅ ALL TABLES UPDATED**
|
||||
**✅ ALL FORMS UPDATED**
|
||||
**✅ ALL FILTERS UPDATED**
|
||||
**✅ ALL ADMIN INTERFACES UPDATED**
|
||||
**✅ ALL API ENDPOINTS UPDATED**
|
||||
|
||||
**Status: PRODUCTION READY** 🎉
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# WordPress Integration Fixes
|
||||
|
||||
## Issues Fixed
|
||||
|
||||
### 1. Validation Error (400 Bad Request)
|
||||
**Problem**: When creating WordPress integration with API key-only authentication, the backend was rejecting the request because `username` and `app_password` were empty strings.
|
||||
|
||||
**Root Cause**: The serializer was validating credentials but didn't account for API key-only authentication where username/password are optional.
|
||||
|
||||
**Fix**: Added custom validation in `SiteIntegrationSerializer` to allow API key-only authentication for WordPress platform:
|
||||
- If `api_key` is provided in `credentials_json`, username and app_password are optional
|
||||
- If `api_key` is not provided, username and app_password are required (traditional auth)
|
||||
|
||||
**File**: `backend/igny8_core/modules/integration/views.py`
|
||||
|
||||
### 2. Status Indicator Not Showing Connected
|
||||
**Problem**: Status indicator showed "Not configured" even when integration existed and was active.
|
||||
|
||||
**Root Cause**: Status check only looked for `site?.wp_api_key` but didn't check for API key in integration's `credentials_json`.
|
||||
|
||||
**Fix**: Updated status check to look for API key in both:
|
||||
- Site's `wp_api_key` field
|
||||
- Integration's `credentials_json.api_key` field
|
||||
|
||||
**File**: `frontend/src/pages/Sites/Settings.tsx`
|
||||
|
||||
### 3. Integration Creation Error Handling
|
||||
**Problem**: When toggling integration enabled without API key, no clear error was shown.
|
||||
|
||||
**Fix**: Added error handling to show clear message when trying to enable integration without API key.
|
||||
|
||||
**File**: `frontend/src/components/sites/WordPressIntegrationForm.tsx`
|
||||
|
||||
## Content Sync Status
|
||||
|
||||
Content sync will work as long as:
|
||||
1. ✅ Integration exists in database
|
||||
2. ✅ Integration `is_active = True`
|
||||
3. ✅ Integration `sync_enabled = True`
|
||||
|
||||
The sync service checks these conditions before performing sync operations.
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [x] Create WordPress integration with API key only (no username/password)
|
||||
- [x] Status indicator shows "Configured" when integration exists and is active
|
||||
- [x] Status indicator shows "Connected" after successful connection test
|
||||
- [x] Content sync works when integration is active and sync_enabled
|
||||
- [x] Error messages are clear when API key is missing
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
# IGNY8 Content Architecture Migration Summary
|
||||
|
||||
**Date**: November 21, 2025
|
||||
**Status**: ✅ **COMPLETED SUCCESSFULLY**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Complete migration from fragmented content/taxonomy structure to unified WordPress-ready architecture.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: New Models & Fields ✅
|
||||
|
||||
### New Models Created
|
||||
|
||||
#### 1. `ContentTaxonomy` (`igny8_content_taxonomy_terms`)
|
||||
Unified taxonomy model for categories, tags, and product attributes.
|
||||
|
||||
**Key Fields:**
|
||||
- `name`, `slug`, `taxonomy_type` (category, tag, product_cat, product_tag, product_attr, service_cat)
|
||||
- `external_id`, `external_taxonomy` (WordPress sync fields)
|
||||
- `sync_status` (native, imported, synced)
|
||||
- `count` (post count from WP)
|
||||
- `parent` (hierarchical taxonomies)
|
||||
- M2M to `Clusters` (semantic mapping)
|
||||
|
||||
**Indexes:** 14 total including composite indexes for WP sync lookups
|
||||
|
||||
#### 2. `ContentAttribute` (`igny8_content_attributes`)
|
||||
Renamed from `ContentAttributeMap` with enhanced WP sync support.
|
||||
|
||||
**Key Fields:**
|
||||
- `attribute_type` (product_spec, service_modifier, semantic_facet)
|
||||
- `name`, `value`
|
||||
- `external_id`, `external_attribute_name` (WooCommerce sync)
|
||||
- FK to `Content`, `Cluster`
|
||||
|
||||
**Indexes:** 7 total for efficient attribute lookups
|
||||
|
||||
#### 3. `ContentTaxonomyRelation` (`igny8_content_taxonomy_relations`)
|
||||
Through model for Content ↔ ContentTaxonomy M2M.
|
||||
|
||||
**Note:** Simplified to avoid tenant_id constraint issues.
|
||||
|
||||
### Content Model Enhancements
|
||||
|
||||
**New Fields:**
|
||||
- `content_format` (article, listicle, guide, comparison, review, roundup)
|
||||
- `cluster_role` (hub, supporting, attribute)
|
||||
- `external_type` (WP post type: post, page, product, service)
|
||||
- `cluster` FK (direct cluster relationship)
|
||||
- `taxonomies` M2M (replaces JSON categories/tags)
|
||||
|
||||
**Updated Fields:**
|
||||
- `entity_type` now uses: post, page, product, service, taxonomy_term (legacy values preserved)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Data Migration ✅
|
||||
|
||||
### Migrations Performed
|
||||
|
||||
1. **Content Entity Types** (`migrate_content_entity_types`)
|
||||
- Converted legacy `blog_post` → `post` + `content_format='article'`
|
||||
- Converted `article` → `post` + `content_format='article'`
|
||||
- Converted `taxonomy` → `taxonomy_term`
|
||||
|
||||
2. **Task Entity Types** (`migrate_task_entity_types`)
|
||||
- Migrated `Tasks.entity_type` → `Content.entity_type` + `content_format`
|
||||
- Migrated `Tasks.cluster_role` → `Content.cluster_role`
|
||||
- Migrated `Tasks.cluster_id` → `Content.cluster_id`
|
||||
|
||||
3. **Categories & Tags** (`migrate_content_categories_tags_to_taxonomy`)
|
||||
- Converted `Content.categories` JSON → `ContentTaxonomy` records (type: category)
|
||||
- Converted `Content.tags` JSON → `ContentTaxonomy` records (type: tag)
|
||||
- Created M2M relationships via `ContentTaxonomyRelation`
|
||||
|
||||
4. **Blueprint Taxonomies** (`migrate_blueprint_taxonomies`)
|
||||
- Migrated `SiteBlueprintTaxonomy` → `ContentTaxonomy`
|
||||
- Preserved `external_reference` as `external_id`
|
||||
- Preserved cluster mappings
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Deprecation & Cleanup ✅
|
||||
|
||||
### Deprecated Fields (Marked, Not Removed)
|
||||
|
||||
**In `Tasks` model:**
|
||||
- `content` → Use `Content.html_content`
|
||||
- `word_count` → Use `Content.word_count`
|
||||
- `meta_title` → Use `Content.meta_title`
|
||||
- `meta_description` → Use `Content.meta_description`
|
||||
- `assigned_post_id` → Use `Content.external_id`
|
||||
- `post_url` → Use `Content.external_url`
|
||||
- `entity_type` → Use `Content.entity_type`
|
||||
- `cluster_role` → Use `Content.cluster_role`
|
||||
- `content_structure` → Merged into `Content.content_format`
|
||||
- `content_type` → Merged into `Content.entity_type + content_format`
|
||||
|
||||
**In `Content` model:**
|
||||
- `categories` → Use `Content.taxonomies` M2M
|
||||
- `tags` → Use `Content.taxonomies` M2M
|
||||
|
||||
**Reason for Preservation:** Backward compatibility during transition period. Can be removed in future migration after ensuring no dependencies.
|
||||
|
||||
### Blueprint Tables Status
|
||||
|
||||
Tables **preserved** (1 active blueprint found):
|
||||
- `igny8_site_blueprints`
|
||||
- `igny8_page_blueprints`
|
||||
- `igny8_site_blueprint_clusters`
|
||||
- `igny8_site_blueprint_taxonomies`
|
||||
|
||||
**Note:** These can be dropped in Phase 4 if/when site builder is fully replaced by WP import flow.
|
||||
|
||||
---
|
||||
|
||||
## Applied Migrations
|
||||
|
||||
```
|
||||
writer
|
||||
[X] 0001_initial
|
||||
[X] 0002_phase1_add_unified_taxonomy_and_attributes
|
||||
[X] 0003_phase1b_fix_taxonomy_relation
|
||||
[X] 0004_phase2_migrate_data_to_unified_structure
|
||||
[X] 0005_phase3_mark_deprecated_fields
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Serializers Updated ✅
|
||||
|
||||
### New Serializers Created
|
||||
|
||||
1. `ContentTaxonomySerializer`
|
||||
- Includes parent_name, cluster_names, content_count
|
||||
- Full CRUD support
|
||||
|
||||
2. `ContentAttributeSerializer`
|
||||
- Includes content_title, cluster_name
|
||||
- WP sync field support
|
||||
|
||||
3. `ContentTaxonomyRelationSerializer`
|
||||
- M2M relationship details
|
||||
- Read-only access to relation metadata
|
||||
|
||||
### Existing Serializers Updated
|
||||
|
||||
- `TasksSerializer`: Updated to use `ContentAttribute` (backward compatible alias)
|
||||
- `ContentSerializer`: Updated attribute mappings to use new model
|
||||
|
||||
---
|
||||
|
||||
## Database Verification ✅
|
||||
|
||||
### New Tables Confirmed
|
||||
|
||||
```sql
|
||||
✓ igny8_content_taxonomy_terms (16 columns, 23 indexes)
|
||||
✓ igny8_content_attributes (16 columns, 15 indexes)
|
||||
✓ igny8_content_taxonomy_relations (4 columns, 3 indexes)
|
||||
✓ igny8_content_taxonomy_terms_clusters (M2M table)
|
||||
```
|
||||
|
||||
### New Content Fields Confirmed
|
||||
|
||||
```sql
|
||||
✓ cluster_id (bigint)
|
||||
✓ cluster_role (varchar)
|
||||
✓ content_format (varchar)
|
||||
✓ external_type (varchar)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend Status ✅
|
||||
|
||||
**Container:** `igny8_backend`
|
||||
**Status:** Running and healthy
|
||||
**Workers:** 4 gunicorn workers booted successfully
|
||||
**No errors detected in startup logs**
|
||||
|
||||
---
|
||||
|
||||
## WordPress Integration Readiness
|
||||
|
||||
### Ready for WP Sync
|
||||
|
||||
1. **Content Type Detection**
|
||||
- `Content.entity_type` = WP post_type (post, page, product)
|
||||
- `Content.external_type` = source post_type name
|
||||
- `Content.external_id` = WP post ID
|
||||
- `Content.external_url` = WP post permalink
|
||||
|
||||
2. **Taxonomy Sync**
|
||||
- `ContentTaxonomy.external_id` = WP term ID
|
||||
- `ContentTaxonomy.external_taxonomy` = WP taxonomy name (category, post_tag, product_cat, pa_*)
|
||||
- `ContentTaxonomy.taxonomy_type` = mapped type
|
||||
- `ContentTaxonomy.sync_status` = import tracking
|
||||
|
||||
3. **Product Attributes**
|
||||
- `ContentAttribute.external_id` = WooCommerce attribute term ID
|
||||
- `ContentAttribute.external_attribute_name` = WP attribute slug (pa_color, pa_size)
|
||||
- `ContentAttribute.attribute_type` = product_spec
|
||||
|
||||
4. **Semantic Mapping**
|
||||
- `ContentTaxonomy.clusters` M2M = AI cluster assignments
|
||||
- `Content.cluster` FK = primary semantic cluster
|
||||
- `Content.cluster_role` = hub/supporting/attribute
|
||||
|
||||
---
|
||||
|
||||
## Next Steps for WP Integration
|
||||
|
||||
### Immediate (Already Prepared)
|
||||
|
||||
1. ✅ Plugin `/site-metadata/` endpoint exists
|
||||
2. ✅ Database structure ready
|
||||
3. ✅ Models & serializers ready
|
||||
|
||||
### Phase 4 (Next Session)
|
||||
|
||||
1. **Backend Service Layer**
|
||||
- `IntegrationService.fetch_content_structure(integration_id)`
|
||||
- `IntegrationService.import_taxonomies(integration_id, taxonomy_type, limit)`
|
||||
- `IntegrationService.import_content_titles(integration_id, post_type, limit)`
|
||||
- `IntegrationService.fetch_full_content(content_id)` (on-demand)
|
||||
|
||||
2. **Backend Endpoints**
|
||||
- `POST /api/v1/integration/integrations/{id}/fetch-structure/`
|
||||
- `POST /api/v1/integration/integrations/{id}/import-taxonomies/`
|
||||
- `POST /api/v1/integration/integrations/{id}/import-content/`
|
||||
- `GET /api/v1/integration/content-taxonomies/` (ViewSet)
|
||||
- `GET /api/v1/integration/content-attributes/` (ViewSet)
|
||||
|
||||
3. **Frontend UI**
|
||||
- New tab: "Content Types" in Site Settings
|
||||
- Display detected post types & taxonomies
|
||||
- Enable/disable toggles
|
||||
- Fetch limit inputs
|
||||
- Sync status indicators
|
||||
|
||||
4. **AI Semantic Mapping**
|
||||
- Endpoint: `POST /api/v1/integration/integrations/{id}/generate-semantic-map/`
|
||||
- Input: Content titles + taxonomy terms
|
||||
- Output: Cluster recommendations + attribute suggestions
|
||||
- Auto-create clusters and map taxonomies
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan (If Needed)
|
||||
|
||||
### Critical Data Preserved
|
||||
|
||||
- ✅ Original JSON categories/tags still in Content table
|
||||
- ✅ Original blueprint taxonomies table intact
|
||||
- ✅ Legacy entity_type values preserved in choices
|
||||
- ✅ All task fields still functional
|
||||
|
||||
### To Rollback
|
||||
|
||||
```bash
|
||||
# Rollback to before migration
|
||||
python manage.py migrate writer 0001
|
||||
|
||||
# Remove new tables manually if needed
|
||||
DROP TABLE igny8_content_taxonomy_relations CASCADE;
|
||||
DROP TABLE igny8_content_taxonomy_terms_clusters CASCADE;
|
||||
DROP TABLE igny8_content_taxonomy_terms CASCADE;
|
||||
DROP TABLE igny8_content_attributes CASCADE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- All new tables have appropriate indexes
|
||||
- Composite indexes for WP sync lookups (external_id + external_taxonomy)
|
||||
- Indexes on taxonomy_type, sync_status for filtering
|
||||
- M2M through table is minimal (no tenant_id to avoid constraint issues)
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Manual Tests
|
||||
|
||||
1. ✅ Backend restart successful
|
||||
2. ✅ Database tables created correctly
|
||||
3. ✅ Migrations applied without errors
|
||||
4. 🔲 Create new ContentTaxonomy via API
|
||||
5. 🔲 Assign taxonomies to content via M2M
|
||||
6. 🔲 Create ContentAttribute for product
|
||||
7. 🔲 Query taxonomies by external_id
|
||||
8. 🔲 Test cluster → taxonomy mapping
|
||||
|
||||
### Integration Tests (Next Phase)
|
||||
|
||||
1. WP `/site-metadata/` → Backend storage
|
||||
2. WP category import → ContentTaxonomy creation
|
||||
3. WP product attribute import → ContentAttribute creation
|
||||
4. Content → Taxonomy M2M assignment
|
||||
5. AI semantic mapping with imported data
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**All 3 phases completed successfully:**
|
||||
|
||||
✅ **Phase 1**: New models & fields added
|
||||
✅ **Phase 2**: Existing data migrated
|
||||
✅ **Phase 3**: Deprecated fields marked
|
||||
|
||||
**Current Status**: Production-ready, backward compatible, WordPress integration prepared.
|
||||
|
||||
**Zero downtime**: All changes non-breaking, existing functionality preserved.
|
||||
|
||||
---
|
||||
|
||||
**Migration Completed By**: AI Assistant
|
||||
**Total Migrations**: 5
|
||||
**Total New Tables**: 4
|
||||
**Total New Fields in Content**: 4
|
||||
**Deprecated Fields**: 12 (marked, not removed)
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
# IGNY8 Unified Content Architecture - Quick Reference
|
||||
|
||||
## ✅ What Changed
|
||||
|
||||
### Old Way ❌
|
||||
```python
|
||||
# Scattered entity types
|
||||
task.entity_type = 'blog_post'
|
||||
task.content_type = 'article'
|
||||
task.content_structure = 'pillar_page'
|
||||
|
||||
# JSON arrays for taxonomies
|
||||
content.categories = ['SEO', 'WordPress']
|
||||
content.tags = ['tutorial', 'guide']
|
||||
|
||||
# Fragmented attributes
|
||||
ContentAttributeMap(name='Color', value='Blue')
|
||||
```
|
||||
|
||||
### New Way ✅
|
||||
```python
|
||||
# Single unified entity type
|
||||
content.entity_type = 'post' # What it is
|
||||
content.content_format = 'article' # How it's structured
|
||||
content.cluster_role = 'hub' # Semantic role
|
||||
|
||||
# Real M2M relationships
|
||||
content.taxonomies.add(seo_category)
|
||||
content.taxonomies.add(tutorial_tag)
|
||||
|
||||
# Enhanced attributes with WP sync
|
||||
ContentAttribute(
|
||||
content=content,
|
||||
attribute_type='product_spec',
|
||||
name='Color',
|
||||
value='Blue',
|
||||
external_id=101, # WP term ID
|
||||
external_attribute_name='pa_color'
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Core Models
|
||||
|
||||
### 1. Content (Enhanced)
|
||||
```python
|
||||
from igny8_core.business.content.models import Content
|
||||
|
||||
# Create content
|
||||
content = Content.objects.create(
|
||||
title="Best SEO Tools 2025",
|
||||
entity_type='post', # post, page, product, service, taxonomy_term
|
||||
content_format='listicle', # article, listicle, guide, comparison, review
|
||||
cluster_role='hub', # hub, supporting, attribute
|
||||
html_content="<h1>Best SEO Tools...</h1>",
|
||||
|
||||
# WordPress sync
|
||||
external_id=427, # WP post ID
|
||||
external_url="https://site.com/seo-tools/",
|
||||
external_type='post', # WP post_type
|
||||
source='wordpress',
|
||||
sync_status='imported',
|
||||
|
||||
# SEO
|
||||
meta_title="15 Best SEO Tools...",
|
||||
primary_keyword="seo tools",
|
||||
|
||||
# Relationships
|
||||
cluster=seo_cluster,
|
||||
site=site,
|
||||
sector=sector,
|
||||
)
|
||||
|
||||
# Add taxonomies
|
||||
content.taxonomies.add(seo_category, tools_tag)
|
||||
```
|
||||
|
||||
### 2. ContentTaxonomy (New)
|
||||
```python
|
||||
from igny8_core.business.content.models import ContentTaxonomy
|
||||
|
||||
# WordPress category
|
||||
category = ContentTaxonomy.objects.create(
|
||||
name="SEO",
|
||||
slug="seo",
|
||||
taxonomy_type='category', # category, tag, product_cat, product_tag, product_attr
|
||||
description="All about SEO",
|
||||
|
||||
# WordPress sync
|
||||
external_id=12, # WP term ID
|
||||
external_taxonomy='category', # WP taxonomy name
|
||||
sync_status='imported',
|
||||
count=45, # Post count from WP
|
||||
|
||||
site=site,
|
||||
sector=sector,
|
||||
)
|
||||
|
||||
# Map to semantic clusters
|
||||
category.clusters.add(seo_cluster, content_marketing_cluster)
|
||||
|
||||
# Hierarchical taxonomy
|
||||
subcategory = ContentTaxonomy.objects.create(
|
||||
name="Technical SEO",
|
||||
slug="technical-seo",
|
||||
taxonomy_type='category',
|
||||
parent=category, # Parent category
|
||||
site=site,
|
||||
sector=sector,
|
||||
)
|
||||
```
|
||||
|
||||
### 3. ContentAttribute (Enhanced)
|
||||
```python
|
||||
from igny8_core.business.content.models import ContentAttribute
|
||||
|
||||
# WooCommerce product attribute
|
||||
attribute = ContentAttribute.objects.create(
|
||||
content=product_content,
|
||||
attribute_type='product_spec', # product_spec, service_modifier, semantic_facet
|
||||
name='Color',
|
||||
value='Blue',
|
||||
|
||||
# WooCommerce sync
|
||||
external_id=101, # WP attribute term ID
|
||||
external_attribute_name='pa_color', # WP attribute slug
|
||||
|
||||
source='wordpress',
|
||||
site=site,
|
||||
sector=sector,
|
||||
)
|
||||
|
||||
# Semantic cluster attribute
|
||||
semantic_attr = ContentAttribute.objects.create(
|
||||
cluster=enterprise_seo_cluster,
|
||||
attribute_type='semantic_facet',
|
||||
name='Target Audience',
|
||||
value='Enterprise',
|
||||
source='manual',
|
||||
site=site,
|
||||
sector=sector,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 WordPress Sync Workflows
|
||||
|
||||
### Scenario 1: Import WP Categories
|
||||
```python
|
||||
from igny8_core.business.content.models import ContentTaxonomy
|
||||
|
||||
# Fetch from WP /wp-json/wp/v2/categories
|
||||
wp_categories = [
|
||||
{'id': 12, 'name': 'SEO', 'slug': 'seo', 'count': 45},
|
||||
{'id': 15, 'name': 'WordPress', 'slug': 'wordpress', 'count': 32},
|
||||
]
|
||||
|
||||
for wp_cat in wp_categories:
|
||||
taxonomy, created = ContentTaxonomy.objects.update_or_create(
|
||||
site=site,
|
||||
external_id=wp_cat['id'],
|
||||
external_taxonomy='category',
|
||||
defaults={
|
||||
'name': wp_cat['name'],
|
||||
'slug': wp_cat['slug'],
|
||||
'taxonomy_type': 'category',
|
||||
'count': wp_cat['count'],
|
||||
'sync_status': 'imported',
|
||||
'sector': site.sectors.first(),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Scenario 2: Import WP Posts (Titles Only)
|
||||
```python
|
||||
from igny8_core.business.content.models import Content, ContentTaxonomy
|
||||
|
||||
# Fetch from WP /wp-json/wp/v2/posts
|
||||
wp_posts = [
|
||||
{
|
||||
'id': 427,
|
||||
'title': {'rendered': 'Best SEO Tools 2025'},
|
||||
'link': 'https://site.com/seo-tools/',
|
||||
'type': 'post',
|
||||
'categories': [12, 15],
|
||||
'tags': [45, 67],
|
||||
}
|
||||
]
|
||||
|
||||
for wp_post in wp_posts:
|
||||
# Create content (title only, no html_content yet)
|
||||
content, created = Content.objects.update_or_create(
|
||||
site=site,
|
||||
external_id=wp_post['id'],
|
||||
defaults={
|
||||
'title': wp_post['title']['rendered'],
|
||||
'entity_type': 'post',
|
||||
'external_url': wp_post['link'],
|
||||
'external_type': wp_post['type'],
|
||||
'source': 'wordpress',
|
||||
'sync_status': 'imported',
|
||||
'sector': site.sectors.first(),
|
||||
}
|
||||
)
|
||||
|
||||
# Map categories
|
||||
for cat_id in wp_post['categories']:
|
||||
try:
|
||||
taxonomy = ContentTaxonomy.objects.get(
|
||||
site=site,
|
||||
external_id=cat_id,
|
||||
taxonomy_type='category'
|
||||
)
|
||||
content.taxonomies.add(taxonomy)
|
||||
except ContentTaxonomy.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Map tags
|
||||
for tag_id in wp_post['tags']:
|
||||
try:
|
||||
taxonomy = ContentTaxonomy.objects.get(
|
||||
site=site,
|
||||
external_id=tag_id,
|
||||
taxonomy_type='tag'
|
||||
)
|
||||
content.taxonomies.add(taxonomy)
|
||||
except ContentTaxonomy.DoesNotExist:
|
||||
pass
|
||||
```
|
||||
|
||||
### Scenario 3: Fetch Full Content On-Demand
|
||||
```python
|
||||
def fetch_full_content(content_id):
|
||||
"""Fetch full HTML content from WP when needed for AI analysis."""
|
||||
content = Content.objects.get(id=content_id)
|
||||
|
||||
if content.source == 'wordpress' and content.external_id:
|
||||
# Fetch from WP /wp-json/wp/v2/posts/{external_id}
|
||||
wp_response = requests.get(
|
||||
f"{content.site.url}/wp-json/wp/v2/posts/{content.external_id}"
|
||||
)
|
||||
wp_data = wp_response.json()
|
||||
|
||||
# Update content
|
||||
content.html_content = wp_data['content']['rendered']
|
||||
content.word_count = len(wp_data['content']['rendered'].split())
|
||||
content.meta_title = wp_data.get('yoast_head_json', {}).get('title', '')
|
||||
content.meta_description = wp_data.get('yoast_head_json', {}).get('description', '')
|
||||
content.save()
|
||||
|
||||
return content
|
||||
```
|
||||
|
||||
### Scenario 4: Import WooCommerce Product Attributes
|
||||
```python
|
||||
from igny8_core.business.content.models import Content, ContentAttribute
|
||||
|
||||
# Fetch from WP /wp-json/wc/v3/products/{id}
|
||||
wp_product = {
|
||||
'id': 88,
|
||||
'name': 'Blue Widget',
|
||||
'type': 'simple',
|
||||
'attributes': [
|
||||
{'id': 1, 'name': 'Color', 'slug': 'pa_color', 'option': 'Blue'},
|
||||
{'id': 2, 'name': 'Size', 'slug': 'pa_size', 'option': 'Large'},
|
||||
]
|
||||
}
|
||||
|
||||
# Create product content
|
||||
product = Content.objects.create(
|
||||
site=site,
|
||||
title=wp_product['name'],
|
||||
entity_type='product',
|
||||
external_id=wp_product['id'],
|
||||
external_type='product',
|
||||
source='wordpress',
|
||||
sync_status='imported',
|
||||
sector=site.sectors.first(),
|
||||
)
|
||||
|
||||
# Import attributes
|
||||
for attr in wp_product['attributes']:
|
||||
ContentAttribute.objects.create(
|
||||
content=product,
|
||||
attribute_type='product_spec',
|
||||
name=attr['name'],
|
||||
value=attr['option'],
|
||||
external_attribute_name=attr['slug'],
|
||||
source='wordpress',
|
||||
site=site,
|
||||
sector=site.sectors.first(),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Query Examples
|
||||
|
||||
### Find Content by Entity Type
|
||||
```python
|
||||
# All blog posts
|
||||
posts = Content.objects.filter(entity_type='post')
|
||||
|
||||
# All listicles
|
||||
listicles = Content.objects.filter(entity_type='post', content_format='listicle')
|
||||
|
||||
# All hub pages
|
||||
hubs = Content.objects.filter(cluster_role='hub')
|
||||
|
||||
# All WP-synced products
|
||||
products = Content.objects.filter(
|
||||
entity_type='product',
|
||||
source='wordpress',
|
||||
sync_status='imported'
|
||||
)
|
||||
```
|
||||
|
||||
### Find Taxonomies
|
||||
```python
|
||||
# All categories with WP sync
|
||||
categories = ContentTaxonomy.objects.filter(
|
||||
taxonomy_type='category',
|
||||
external_id__isnull=False
|
||||
)
|
||||
|
||||
# Product attributes (color, size, etc.)
|
||||
product_attrs = ContentTaxonomy.objects.filter(taxonomy_type='product_attr')
|
||||
|
||||
# Taxonomies mapped to a cluster
|
||||
cluster_terms = ContentTaxonomy.objects.filter(clusters=seo_cluster)
|
||||
|
||||
# Get all content for a taxonomy
|
||||
seo_content = Content.objects.filter(taxonomies=seo_category)
|
||||
```
|
||||
|
||||
### Find Attributes
|
||||
```python
|
||||
# All product specs for a content
|
||||
specs = ContentAttribute.objects.filter(
|
||||
content=product,
|
||||
attribute_type='product_spec'
|
||||
)
|
||||
|
||||
# All attributes in a cluster
|
||||
cluster_attrs = ContentAttribute.objects.filter(
|
||||
cluster=enterprise_cluster,
|
||||
attribute_type='semantic_facet'
|
||||
)
|
||||
|
||||
# Find content by attribute value
|
||||
blue_products = Content.objects.filter(
|
||||
attributes__name='Color',
|
||||
attributes__value='Blue'
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Relationships Diagram
|
||||
|
||||
```
|
||||
Site
|
||||
├─ Content (post, page, product, service, taxonomy_term)
|
||||
│ ├─ entity_type (what it is)
|
||||
│ ├─ content_format (how it's structured)
|
||||
│ ├─ cluster_role (semantic role)
|
||||
│ ├─ cluster FK → Clusters
|
||||
│ ├─ taxonomies M2M → ContentTaxonomy
|
||||
│ └─ attributes FK ← ContentAttribute
|
||||
│
|
||||
├─ ContentTaxonomy (category, tag, product_cat, product_tag, product_attr)
|
||||
│ ├─ external_id (WP term ID)
|
||||
│ ├─ external_taxonomy (WP taxonomy name)
|
||||
│ ├─ parent FK → self (hierarchical)
|
||||
│ ├─ clusters M2M → Clusters
|
||||
│ └─ contents M2M ← Content
|
||||
│
|
||||
└─ Clusters
|
||||
├─ contents FK ← Content
|
||||
├─ taxonomy_terms M2M ← ContentTaxonomy
|
||||
└─ attributes FK ← ContentAttribute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Migration Notes
|
||||
|
||||
### Deprecated Fields (Still Available)
|
||||
|
||||
**Don't use these anymore:**
|
||||
```python
|
||||
# ❌ Old way
|
||||
task.content = "..." # Use Content.html_content
|
||||
task.entity_type = "..." # Use Content.entity_type
|
||||
content.categories = ["SEO"] # Use content.taxonomies M2M
|
||||
content.tags = ["tutorial"] # Use content.taxonomies M2M
|
||||
```
|
||||
|
||||
**Use these instead:**
|
||||
```python
|
||||
# ✅ New way
|
||||
content.html_content = "..."
|
||||
content.entity_type = "post"
|
||||
content.taxonomies.add(seo_category)
|
||||
content.taxonomies.add(tutorial_tag)
|
||||
```
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
Legacy values still work:
|
||||
```python
|
||||
# These still map correctly
|
||||
content.entity_type = 'blog_post' # → internally handled as 'post'
|
||||
content.entity_type = 'article' # → internally handled as 'post'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next: Frontend Integration
|
||||
|
||||
Ready for Phase 4:
|
||||
1. Site Settings → "Content Types" tab
|
||||
2. Display imported taxonomies
|
||||
3. Enable/disable sync per type
|
||||
4. Set fetch limits
|
||||
5. Trigger AI semantic mapping
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check `/data/app/igny8/backend/MIGRATION_SUMMARY.md` for full migration details.
|
||||
|
||||
@@ -1,705 +0,0 @@
|
||||
# Sites Integration Plan - Content Types Structure
|
||||
|
||||
**Date**: November 22, 2025
|
||||
**Status**: 📋 **PLANNING**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Integrate the new unified content architecture (ContentTaxonomy, ContentAttribute, entity_type, content_format) with the Sites module and SiteIntegration model to enable WordPress content type discovery, configuration, and sync.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### ✅ What We Have
|
||||
|
||||
**1. Unified Content Architecture (COMPLETE)**
|
||||
- `Content` model with `entity_type`, `content_format`, `cluster_role`
|
||||
- `ContentTaxonomy` model for categories, tags, product attributes
|
||||
- `ContentAttribute` model for product specs, service modifiers
|
||||
- WordPress sync fields (`external_id`, `external_taxonomy`, `sync_status`)
|
||||
|
||||
**2. Site Model**
|
||||
- Basic site information (name, domain, industry)
|
||||
- `site_type` field (marketing, ecommerce, blog, portfolio, corporate)
|
||||
- `hosting_type` field (igny8_sites, wordpress, shopify, multi)
|
||||
- Legacy WP fields (`wp_url`, `wp_username`, `wp_api_key`)
|
||||
|
||||
**3. SiteIntegration Model**
|
||||
- Platform-specific integrations (wordpress, shopify, custom)
|
||||
- `config_json` for configuration
|
||||
- `credentials_json` for API keys/tokens
|
||||
- `sync_enabled` flag for two-way sync
|
||||
|
||||
**4. WordPress Plugin**
|
||||
- `/wp-json/igny8/v1/site-metadata/` endpoint
|
||||
- Returns post types, taxonomies, and counts
|
||||
- API key authentication support
|
||||
|
||||
### ❌ What's Missing
|
||||
|
||||
1. **Content Type Configuration Storage**
|
||||
- No place to store which post types/taxonomies are enabled
|
||||
- No fetch limits per content type
|
||||
- No sync preferences per taxonomy
|
||||
|
||||
2. **Site → Integration Connection**
|
||||
- No clear link between Site.site_type and available content types
|
||||
- No mapping of WP post types to IGNY8 entity types
|
||||
|
||||
3. **Frontend UI**
|
||||
- No "Content Types" tab in Site Settings
|
||||
- No interface to enable/disable content types
|
||||
- No way to set fetch limits
|
||||
|
||||
4. **Backend Service Methods**
|
||||
- No method to fetch WP structure and store in `config_json`
|
||||
- No method to import taxonomies
|
||||
- No method to import content titles
|
||||
|
||||
---
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### Phase 1: Extend SiteIntegration.config_json Structure
|
||||
|
||||
Store WordPress content type configuration in `SiteIntegration.config_json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"api_version": "v1",
|
||||
"plugin_version": "1.0.0",
|
||||
"content_types": {
|
||||
"post_types": {
|
||||
"post": {
|
||||
"label": "Posts",
|
||||
"count": 123,
|
||||
"enabled": true,
|
||||
"fetch_limit": 100,
|
||||
"entity_type": "post",
|
||||
"content_format": "article",
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"page": {
|
||||
"label": "Pages",
|
||||
"count": 12,
|
||||
"enabled": true,
|
||||
"fetch_limit": 50,
|
||||
"entity_type": "page",
|
||||
"content_format": null,
|
||||
"last_synced": null
|
||||
},
|
||||
"product": {
|
||||
"label": "Products",
|
||||
"count": 456,
|
||||
"enabled": true,
|
||||
"fetch_limit": 200,
|
||||
"entity_type": "product",
|
||||
"content_format": null,
|
||||
"last_synced": null
|
||||
}
|
||||
},
|
||||
"taxonomies": {
|
||||
"category": {
|
||||
"label": "Categories",
|
||||
"count": 25,
|
||||
"enabled": true,
|
||||
"fetch_limit": 100,
|
||||
"taxonomy_type": "category",
|
||||
"last_synced": "2025-11-22T10:00:00Z"
|
||||
},
|
||||
"post_tag": {
|
||||
"label": "Tags",
|
||||
"count": 102,
|
||||
"enabled": true,
|
||||
"fetch_limit": 200,
|
||||
"taxonomy_type": "tag",
|
||||
"last_synced": null
|
||||
},
|
||||
"product_cat": {
|
||||
"label": "Product Categories",
|
||||
"count": 15,
|
||||
"enabled": true,
|
||||
"fetch_limit": 50,
|
||||
"taxonomy_type": "product_cat",
|
||||
"last_synced": null
|
||||
},
|
||||
"pa_color": {
|
||||
"label": "Color",
|
||||
"count": 10,
|
||||
"enabled": true,
|
||||
"fetch_limit": 50,
|
||||
"taxonomy_type": "product_attr",
|
||||
"attribute_name": "Color",
|
||||
"last_synced": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugin_connection_enabled": true,
|
||||
"two_way_sync_enabled": true,
|
||||
"last_structure_fetch": "2025-11-22T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Backend Service Methods
|
||||
|
||||
#### 1. **IntegrationService.fetch_content_structure()**
|
||||
|
||||
```python
|
||||
def fetch_content_structure(self, integration_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch content structure from WordPress plugin and store in config_json.
|
||||
|
||||
Steps:
|
||||
1. GET /wp-json/igny8/v1/site-metadata/
|
||||
2. Parse response
|
||||
3. Update integration.config_json['content_types']
|
||||
4. Return structure
|
||||
"""
|
||||
integration = SiteIntegration.objects.get(id=integration_id)
|
||||
|
||||
# Call WordPress plugin
|
||||
wp_url = integration.config_json.get('url')
|
||||
api_key = integration.credentials_json.get('api_key')
|
||||
|
||||
response = requests.get(
|
||||
f"{wp_url}/wp-json/igny8/v1/site-metadata/",
|
||||
headers={'X-IGNY8-API-KEY': api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()['data']
|
||||
|
||||
# Transform to our structure
|
||||
content_types = {
|
||||
'post_types': {},
|
||||
'taxonomies': {}
|
||||
}
|
||||
|
||||
# Map post types
|
||||
for wp_type, info in data['post_types'].items():
|
||||
content_types['post_types'][wp_type] = {
|
||||
'label': info['label'],
|
||||
'count': info['count'],
|
||||
'enabled': False, # Default disabled
|
||||
'fetch_limit': 100, # Default limit
|
||||
'entity_type': self._map_wp_type_to_entity(wp_type),
|
||||
'content_format': None,
|
||||
'last_synced': None
|
||||
}
|
||||
|
||||
# Map taxonomies
|
||||
for wp_tax, info in data['taxonomies'].items():
|
||||
content_types['taxonomies'][wp_tax] = {
|
||||
'label': info['label'],
|
||||
'count': info['count'],
|
||||
'enabled': False, # Default disabled
|
||||
'fetch_limit': 100, # Default limit
|
||||
'taxonomy_type': self._map_wp_tax_to_type(wp_tax),
|
||||
'last_synced': None
|
||||
}
|
||||
|
||||
# Update config
|
||||
if 'content_types' not in integration.config_json:
|
||||
integration.config_json['content_types'] = {}
|
||||
|
||||
integration.config_json['content_types'] = content_types
|
||||
integration.config_json['last_structure_fetch'] = timezone.now().isoformat()
|
||||
integration.save()
|
||||
|
||||
return content_types
|
||||
else:
|
||||
raise Exception(f"Failed to fetch structure: {response.status_code}")
|
||||
|
||||
def _map_wp_type_to_entity(self, wp_type: str) -> str:
|
||||
"""Map WordPress post type to IGNY8 entity_type"""
|
||||
mapping = {
|
||||
'post': 'post',
|
||||
'page': 'page',
|
||||
'product': 'product',
|
||||
'service': 'service',
|
||||
}
|
||||
return mapping.get(wp_type, 'post')
|
||||
|
||||
def _map_wp_tax_to_type(self, wp_tax: str) -> str:
|
||||
"""Map WordPress taxonomy to ContentTaxonomy type"""
|
||||
mapping = {
|
||||
'category': 'category',
|
||||
'post_tag': 'tag',
|
||||
'product_cat': 'product_cat',
|
||||
'product_tag': 'product_tag',
|
||||
}
|
||||
|
||||
# Product attributes start with pa_
|
||||
if wp_tax.startswith('pa_'):
|
||||
return 'product_attr'
|
||||
|
||||
return mapping.get(wp_tax, 'category')
|
||||
```
|
||||
|
||||
#### 2. **IntegrationService.import_taxonomies()**
|
||||
|
||||
```python
|
||||
def import_taxonomies(
|
||||
self,
|
||||
integration_id: int,
|
||||
taxonomy_type: str = None,
|
||||
limit: int = None
|
||||
) -> int:
|
||||
"""
|
||||
Import taxonomy terms from WordPress to ContentTaxonomy.
|
||||
|
||||
Args:
|
||||
integration_id: SiteIntegration ID
|
||||
taxonomy_type: Specific taxonomy to import (e.g., 'category', 'post_tag')
|
||||
limit: Max terms to import per taxonomy
|
||||
|
||||
Returns:
|
||||
Number of terms imported
|
||||
"""
|
||||
integration = SiteIntegration.objects.get(id=integration_id)
|
||||
site = integration.site
|
||||
|
||||
# Get enabled taxonomies from config
|
||||
content_types = integration.config_json.get('content_types', {})
|
||||
taxonomies = content_types.get('taxonomies', {})
|
||||
|
||||
imported_count = 0
|
||||
|
||||
for wp_tax, config in taxonomies.items():
|
||||
# Skip if not enabled or not requested
|
||||
if not config.get('enabled'):
|
||||
continue
|
||||
if taxonomy_type and wp_tax != taxonomy_type:
|
||||
continue
|
||||
|
||||
# Fetch from WordPress
|
||||
fetch_limit = limit or config.get('fetch_limit', 100)
|
||||
wp_url = integration.config_json.get('url')
|
||||
api_key = integration.credentials_json.get('api_key')
|
||||
|
||||
# Map taxonomy endpoint
|
||||
endpoint = self._get_wp_taxonomy_endpoint(wp_tax)
|
||||
|
||||
response = requests.get(
|
||||
f"{wp_url}/wp-json/wp/v2/{endpoint}?per_page={fetch_limit}",
|
||||
headers={'X-IGNY8-API-KEY': api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
terms = response.json()
|
||||
|
||||
for term in terms:
|
||||
# Create or update ContentTaxonomy
|
||||
taxonomy, created = ContentTaxonomy.objects.update_or_create(
|
||||
site=site,
|
||||
external_id=term['id'],
|
||||
external_taxonomy=wp_tax,
|
||||
defaults={
|
||||
'name': term['name'],
|
||||
'slug': term['slug'],
|
||||
'taxonomy_type': config['taxonomy_type'],
|
||||
'description': term.get('description', ''),
|
||||
'count': term.get('count', 0),
|
||||
'sync_status': 'imported',
|
||||
'account': site.account,
|
||||
'sector': site.sectors.first(), # Default to first sector
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
imported_count += 1
|
||||
|
||||
# Update last_synced
|
||||
config['last_synced'] = timezone.now().isoformat()
|
||||
integration.save()
|
||||
|
||||
return imported_count
|
||||
|
||||
def _get_wp_taxonomy_endpoint(self, wp_tax: str) -> str:
|
||||
"""Get WordPress REST endpoint for taxonomy"""
|
||||
mapping = {
|
||||
'category': 'categories',
|
||||
'post_tag': 'tags',
|
||||
'product_cat': 'products/categories',
|
||||
'product_tag': 'products/tags',
|
||||
}
|
||||
|
||||
# Product attributes
|
||||
if wp_tax.startswith('pa_'):
|
||||
attr_id = wp_tax.replace('pa_', '')
|
||||
return f'products/attributes/{attr_id}/terms'
|
||||
|
||||
return mapping.get(wp_tax, wp_tax)
|
||||
```
|
||||
|
||||
#### 3. **IntegrationService.import_content_titles()**
|
||||
|
||||
```python
|
||||
def import_content_titles(
|
||||
self,
|
||||
integration_id: int,
|
||||
post_type: str = None,
|
||||
limit: int = None
|
||||
) -> int:
|
||||
"""
|
||||
Import content titles (not full content) from WordPress.
|
||||
|
||||
Args:
|
||||
integration_id: SiteIntegration ID
|
||||
post_type: Specific post type to import (e.g., 'post', 'product')
|
||||
limit: Max items to import per type
|
||||
|
||||
Returns:
|
||||
Number of content items imported
|
||||
"""
|
||||
integration = SiteIntegration.objects.get(id=integration_id)
|
||||
site = integration.site
|
||||
|
||||
# Get enabled post types from config
|
||||
content_types = integration.config_json.get('content_types', {})
|
||||
post_types = content_types.get('post_types', {})
|
||||
|
||||
imported_count = 0
|
||||
|
||||
for wp_type, config in post_types.items():
|
||||
# Skip if not enabled or not requested
|
||||
if not config.get('enabled'):
|
||||
continue
|
||||
if post_type and wp_type != post_type:
|
||||
continue
|
||||
|
||||
# Fetch from WordPress
|
||||
fetch_limit = limit or config.get('fetch_limit', 100)
|
||||
wp_url = integration.config_json.get('url')
|
||||
api_key = integration.credentials_json.get('api_key')
|
||||
|
||||
# Determine endpoint
|
||||
endpoint = 'products' if wp_type == 'product' else wp_type + 's'
|
||||
|
||||
response = requests.get(
|
||||
f"{wp_url}/wp-json/wp/v2/{endpoint}?per_page={fetch_limit}",
|
||||
headers={'X-IGNY8-API-KEY': api_key}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
items = response.json()
|
||||
|
||||
for item in items:
|
||||
# Create or update Content (title only, no html_content yet)
|
||||
content, created = Content.objects.update_or_create(
|
||||
site=site,
|
||||
external_id=item['id'],
|
||||
external_type=wp_type,
|
||||
defaults={
|
||||
'title': item['title']['rendered'] if isinstance(item['title'], dict) else item['title'],
|
||||
'entity_type': config['entity_type'],
|
||||
'content_format': config.get('content_format'),
|
||||
'external_url': item.get('link', ''),
|
||||
'source': 'wordpress',
|
||||
'sync_status': 'imported',
|
||||
'account': site.account,
|
||||
'sector': site.sectors.first(),
|
||||
}
|
||||
)
|
||||
|
||||
# Map taxonomies
|
||||
if 'categories' in item:
|
||||
for cat_id in item['categories']:
|
||||
try:
|
||||
taxonomy = ContentTaxonomy.objects.get(
|
||||
site=site,
|
||||
external_id=cat_id,
|
||||
taxonomy_type='category'
|
||||
)
|
||||
content.taxonomies.add(taxonomy)
|
||||
except ContentTaxonomy.DoesNotExist:
|
||||
pass
|
||||
|
||||
if 'tags' in item:
|
||||
for tag_id in item['tags']:
|
||||
try:
|
||||
taxonomy = ContentTaxonomy.objects.get(
|
||||
site=site,
|
||||
external_id=tag_id,
|
||||
taxonomy_type='tag'
|
||||
)
|
||||
content.taxonomies.add(taxonomy)
|
||||
except ContentTaxonomy.DoesNotExist:
|
||||
pass
|
||||
|
||||
if created:
|
||||
imported_count += 1
|
||||
|
||||
# Update last_synced
|
||||
config['last_synced'] = timezone.now().isoformat()
|
||||
integration.save()
|
||||
|
||||
return imported_count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Backend API Endpoints
|
||||
|
||||
Add new actions to `IntegrationViewSet`:
|
||||
|
||||
```python
|
||||
@action(detail=True, methods=['post'], url_path='fetch-structure')
|
||||
def fetch_structure(self, request, pk=None):
|
||||
"""
|
||||
POST /api/v1/integration/integrations/{id}/fetch-structure/
|
||||
|
||||
Fetch content type structure from WordPress and store in config.
|
||||
"""
|
||||
integration = self.get_object()
|
||||
service = IntegrationService()
|
||||
|
||||
try:
|
||||
structure = service.fetch_content_structure(integration.id)
|
||||
|
||||
return success_response(
|
||||
data=structure,
|
||||
message="Content structure fetched successfully",
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='import-taxonomies')
|
||||
def import_taxonomies(self, request, pk=None):
|
||||
"""
|
||||
POST /api/v1/integration/integrations/{id}/import-taxonomies/
|
||||
{
|
||||
"taxonomy_type": "category", // optional
|
||||
"limit": 100 // optional
|
||||
}
|
||||
|
||||
Import taxonomy terms from WordPress.
|
||||
"""
|
||||
integration = self.get_object()
|
||||
service = IntegrationService()
|
||||
|
||||
taxonomy_type = request.data.get('taxonomy_type')
|
||||
limit = request.data.get('limit')
|
||||
|
||||
try:
|
||||
count = service.import_taxonomies(integration.id, taxonomy_type, limit)
|
||||
|
||||
return success_response(
|
||||
data={'imported_count': count},
|
||||
message=f"Imported {count} taxonomy terms",
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='import-content')
|
||||
def import_content(self, request, pk=None):
|
||||
"""
|
||||
POST /api/v1/integration/integrations/{id}/import-content/
|
||||
{
|
||||
"post_type": "post", // optional
|
||||
"limit": 100 // optional
|
||||
}
|
||||
|
||||
Import content titles from WordPress.
|
||||
"""
|
||||
integration = self.get_object()
|
||||
service = IntegrationService()
|
||||
|
||||
post_type = request.data.get('post_type')
|
||||
limit = request.data.get('limit')
|
||||
|
||||
try:
|
||||
count = service.import_content_titles(integration.id, post_type, limit)
|
||||
|
||||
return success_response(
|
||||
data={'imported_count': count},
|
||||
message=f"Imported {count} content items",
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=str(e),
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['patch'], url_path='update-content-types')
|
||||
def update_content_types(self, request, pk=None):
|
||||
"""
|
||||
PATCH /api/v1/integration/integrations/{id}/update-content-types/
|
||||
{
|
||||
"post_types": {
|
||||
"post": {"enabled": true, "fetch_limit": 200}
|
||||
},
|
||||
"taxonomies": {
|
||||
"category": {"enabled": true, "fetch_limit": 150}
|
||||
}
|
||||
}
|
||||
|
||||
Update content type configuration.
|
||||
"""
|
||||
integration = self.get_object()
|
||||
|
||||
post_types = request.data.get('post_types', {})
|
||||
taxonomies = request.data.get('taxonomies', {})
|
||||
|
||||
# Update config
|
||||
if 'content_types' not in integration.config_json:
|
||||
integration.config_json['content_types'] = {'post_types': {}, 'taxonomies': {}}
|
||||
|
||||
for wp_type, updates in post_types.items():
|
||||
if wp_type in integration.config_json['content_types']['post_types']:
|
||||
integration.config_json['content_types']['post_types'][wp_type].update(updates)
|
||||
|
||||
for wp_tax, updates in taxonomies.items():
|
||||
if wp_tax in integration.config_json['content_types']['taxonomies']:
|
||||
integration.config_json['content_types']['taxonomies'][wp_tax].update(updates)
|
||||
|
||||
integration.save()
|
||||
|
||||
return success_response(
|
||||
data=integration.config_json['content_types'],
|
||||
message="Content types configuration updated",
|
||||
request=request
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Frontend UI - "Content Types" Tab
|
||||
|
||||
**Location:** Site Settings → Content Types
|
||||
|
||||
**Features:**
|
||||
1. Display fetched content types from `config_json`
|
||||
2. Enable/disable toggles per type
|
||||
3. Fetch limit inputs
|
||||
4. Last synced timestamps
|
||||
5. Sync buttons (Fetch Structure, Import Taxonomies, Import Content)
|
||||
|
||||
**API Calls:**
|
||||
```javascript
|
||||
// Fetch structure
|
||||
POST /api/v1/integration/integrations/{id}/fetch-structure/
|
||||
|
||||
// Update configuration
|
||||
PATCH /api/v1/integration/integrations/{id}/update-content-types/
|
||||
{
|
||||
"post_types": {
|
||||
"post": {"enabled": true, "fetch_limit": 200}
|
||||
}
|
||||
}
|
||||
|
||||
// Import taxonomies
|
||||
POST /api/v1/integration/integrations/{id}/import-taxonomies/
|
||||
|
||||
// Import content
|
||||
POST /api/v1/integration/integrations/{id}/import-content/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Backend Service Methods ✅ READY TO IMPLEMENT
|
||||
- [ ] Add `fetch_content_structure()` to IntegrationService
|
||||
- [ ] Add `import_taxonomies()` to IntegrationService
|
||||
- [ ] Add `import_content_titles()` to IntegrationService
|
||||
- [ ] Add helper methods for WP type mapping
|
||||
|
||||
### Step 2: Backend API Endpoints ✅ READY TO IMPLEMENT
|
||||
- [ ] Add `fetch_structure` action to IntegrationViewSet
|
||||
- [ ] Add `import_taxonomies` action to IntegrationViewSet
|
||||
- [ ] Add `import_content` action to IntegrationViewSet
|
||||
- [ ] Add `update_content_types` action to IntegrationViewSet
|
||||
|
||||
### Step 3: Frontend UI ⏳ PENDING
|
||||
- [ ] Create "Content Types" tab component
|
||||
- [ ] Add post types list with toggles
|
||||
- [ ] Add taxonomies list with toggles
|
||||
- [ ] Add fetch limit inputs
|
||||
- [ ] Add sync buttons
|
||||
- [ ] Add last synced timestamps
|
||||
|
||||
### Step 4: Testing ⏳ PENDING
|
||||
- [ ] Test structure fetch from WP plugin
|
||||
- [ ] Test taxonomy import
|
||||
- [ ] Test content title import
|
||||
- [ ] Test configuration updates
|
||||
- [ ] Test UI interactions
|
||||
|
||||
---
|
||||
|
||||
## Migration Status
|
||||
|
||||
### ✅ Database Ready
|
||||
- All tables exist
|
||||
- All fields exist
|
||||
- All migrations applied
|
||||
|
||||
### ✅ Models Ready
|
||||
- ContentTaxonomy model complete
|
||||
- ContentAttribute model complete
|
||||
- Content model enhanced
|
||||
- SiteIntegration model ready
|
||||
|
||||
### ✅ Admin Ready
|
||||
- All admin interfaces updated
|
||||
- All filters configured
|
||||
|
||||
### ⏳ Services Pending
|
||||
- IntegrationService methods need implementation
|
||||
|
||||
### ⏳ API Endpoints Pending
|
||||
- IntegrationViewSet actions need implementation
|
||||
|
||||
### ⏳ Frontend Pending
|
||||
- Content Types tab needs creation
|
||||
|
||||
---
|
||||
|
||||
## Next Actions
|
||||
|
||||
**IMMEDIATE:**
|
||||
1. Implement IntegrationService methods (fetch_structure, import_taxonomies, import_content_titles)
|
||||
2. Add API endpoints to IntegrationViewSet
|
||||
3. Test with WordPress plugin
|
||||
|
||||
**SOON:**
|
||||
4. Create frontend "Content Types" tab
|
||||
5. Test end-to-end workflow
|
||||
6. Add AI semantic mapping endpoint
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**We are going in the RIGHT direction!** ✅
|
||||
|
||||
The unified content architecture is complete and production-ready. Now we need to:
|
||||
|
||||
1. **Store WP structure** in `SiteIntegration.config_json`
|
||||
2. **Add service methods** to fetch and import from WP
|
||||
3. **Add API endpoints** for frontend to trigger imports
|
||||
4. **Build frontend UI** to manage content types
|
||||
|
||||
The deleted migration file was incorrect (wrong location, wrong approach). The correct approach is to use `SiteIntegration.config_json` to store content type configuration, not database migrations.
|
||||
|
||||
**Status: Ready to implement backend service methods!**
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script to verify field rename is complete
|
||||
Run after SQL migration: python manage.py shell < test_field_rename.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from igny8_core.business.planning.models import ContentIdeas
|
||||
from igny8_core.business.content.models import Tasks, Content
|
||||
from django.db import connection
|
||||
|
||||
def test_models():
|
||||
"""Test that models can access new field names"""
|
||||
print("\n=== Testing Model Field Access ===")
|
||||
|
||||
# Test ContentIdeas
|
||||
try:
|
||||
idea = ContentIdeas.objects.first()
|
||||
if idea:
|
||||
print(f"✓ ContentIdeas.content_type: {idea.content_type}")
|
||||
print(f"✓ ContentIdeas.content_structure: {idea.content_structure}")
|
||||
else:
|
||||
print("⚠ No ContentIdeas records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ ContentIdeas error: {e}")
|
||||
|
||||
# Test Tasks
|
||||
try:
|
||||
task = Tasks.objects.first()
|
||||
if task:
|
||||
print(f"✓ Tasks.content_type: {task.content_type}")
|
||||
print(f"✓ Tasks.content_structure: {task.content_structure}")
|
||||
else:
|
||||
print("⚠ No Tasks records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ Tasks error: {e}")
|
||||
|
||||
# Test Content
|
||||
try:
|
||||
content = Content.objects.first()
|
||||
if content:
|
||||
print(f"✓ Content.content_type: {content.content_type}")
|
||||
print(f"✓ Content.content_structure: {content.content_structure}")
|
||||
print(f"✓ Content.content_html: {len(content.content_html) if content.content_html else 0} chars")
|
||||
else:
|
||||
print("⚠ No Content records to test")
|
||||
except Exception as e:
|
||||
print(f"✗ Content error: {e}")
|
||||
|
||||
def test_database_columns():
|
||||
"""Verify database columns exist"""
|
||||
print("\n=== Testing Database Column Names ===")
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Check igny8_content_ideas
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content_ideas'
|
||||
AND column_name IN ('content_type', 'content_structure', 'site_entity_type', 'cluster_role')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_content_ideas columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols:
|
||||
print("✓ ContentIdeas: new columns exist")
|
||||
if 'site_entity_type' in cols or 'cluster_role' in cols:
|
||||
print("✗ ContentIdeas: old columns still exist")
|
||||
|
||||
# Check igny8_tasks
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_tasks'
|
||||
AND column_name IN ('content_type', 'content_structure', 'entity_type', 'cluster_role')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_tasks columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols:
|
||||
print("✓ Tasks: new columns exist")
|
||||
if 'entity_type' in cols or 'cluster_role' in cols:
|
||||
print("✗ Tasks: old columns still exist")
|
||||
|
||||
# Check igny8_content
|
||||
cursor.execute("""
|
||||
SELECT column_name FROM information_schema.columns
|
||||
WHERE table_name = 'igny8_content'
|
||||
AND column_name IN ('content_type', 'content_structure', 'content_html', 'entity_type', 'cluster_role', 'html_content')
|
||||
ORDER BY column_name
|
||||
""")
|
||||
cols = [row[0] for row in cursor.fetchall()]
|
||||
print(f"igny8_content columns: {cols}")
|
||||
if 'content_type' in cols and 'content_structure' in cols and 'content_html' in cols:
|
||||
print("✓ Content: new columns exist")
|
||||
if 'entity_type' in cols or 'cluster_role' in cols or 'html_content' in cols:
|
||||
print("✗ Content: old columns still exist")
|
||||
|
||||
def test_choices():
|
||||
"""Test that CHOICES are correctly defined"""
|
||||
print("\n=== Testing Model CHOICES ===")
|
||||
|
||||
print(f"ContentIdeas.CONTENT_TYPE_CHOICES: {len(ContentIdeas.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"ContentIdeas.CONTENT_STRUCTURE_CHOICES: {len(ContentIdeas.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
print(f"Tasks.CONTENT_TYPE_CHOICES: {len(Tasks.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"Tasks.CONTENT_STRUCTURE_CHOICES: {len(Tasks.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
print(f"Content.CONTENT_TYPE_CHOICES: {len(Content.CONTENT_TYPE_CHOICES)} types")
|
||||
print(f"Content.CONTENT_STRUCTURE_CHOICES: {len(Content.CONTENT_STRUCTURE_CHOICES)} structures")
|
||||
|
||||
# Verify all 14 structures are present
|
||||
all_structures = [s[0] for s in Tasks.CONTENT_STRUCTURE_CHOICES]
|
||||
expected = ['article', 'guide', 'comparison', 'review', 'listicle',
|
||||
'landing_page', 'business_page', 'service_page', 'general', 'cluster_hub',
|
||||
'product_page', 'category_archive', 'tag_archive', 'attribute_archive']
|
||||
|
||||
for struct in expected:
|
||||
if struct in all_structures:
|
||||
print(f"✓ {struct}")
|
||||
else:
|
||||
print(f"✗ Missing: {struct}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("="*60)
|
||||
print("Field Rename Validation Test")
|
||||
print("="*60)
|
||||
|
||||
test_models()
|
||||
test_database_columns()
|
||||
test_choices()
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("Test Complete")
|
||||
print("="*60)
|
||||
@@ -1,174 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script to run inside Docker container to diagnose generate_content issues
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# Setup Django
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
import logging
|
||||
from django.contrib.auth import get_user_model
|
||||
from igny8_core.auth.models import Account
|
||||
from igny8_core.business.site_building.models import PageBlueprint, SiteBlueprint
|
||||
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
|
||||
from igny8_core.modules.system.models import IntegrationSettings
|
||||
from igny8_core.ai.settings import get_model_config
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("GENERATE_CONTENT DIAGNOSTIC TEST")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. Test User Authentication and get Account
|
||||
print("\n1. Testing User Authentication...")
|
||||
User = get_user_model()
|
||||
user = User.objects.filter(email='dev@igny8.com').first()
|
||||
|
||||
if not user:
|
||||
print("❌ ERROR: User 'dev@igny8.com' not found!")
|
||||
return
|
||||
|
||||
print(f"✓ User found: {user.email} (ID: {user.id})")
|
||||
|
||||
# Get the associated account
|
||||
account = user.account if hasattr(user, 'account') else None
|
||||
if not account:
|
||||
print("❌ ERROR: User has no associated Account!")
|
||||
return
|
||||
|
||||
print(f"✓ Account found: {account.id}")
|
||||
|
||||
# 2. Check Integration Settings
|
||||
print("\n2. Checking Integration Settings...")
|
||||
try:
|
||||
integration = IntegrationSettings.objects.filter(account=account, is_active=True).first()
|
||||
if integration:
|
||||
print(f"✓ Integration found: {integration.integration_type}")
|
||||
print(f" - Config keys: {list(integration.config.keys()) if integration.config else 'None'}")
|
||||
print(f" - Active: {integration.is_active}")
|
||||
else:
|
||||
print("❌ WARNING: No active IntegrationSettings found!")
|
||||
print(" This will cause AI requests to fail!")
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR checking integration: {e}")
|
||||
|
||||
# 3. Test Model Configuration
|
||||
print("\n3. Testing Model Configuration...")
|
||||
try:
|
||||
model_config = get_model_config('generate_page_content', account=account)
|
||||
print(f"✓ Model config loaded:")
|
||||
print(f" - Model: {model_config.get('model')}")
|
||||
print(f" - Max tokens: {model_config.get('max_tokens')}")
|
||||
print(f" - Temperature: {model_config.get('temperature')}")
|
||||
except Exception as e:
|
||||
print(f"❌ ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 4. Check for Site Blueprints
|
||||
print("\n4. Checking for Site Blueprints...")
|
||||
site_blueprints = SiteBlueprint.objects.filter(account=account)
|
||||
print(f" - Found {site_blueprints.count()} site blueprints")
|
||||
|
||||
if site_blueprints.exists():
|
||||
sb = site_blueprints.first()
|
||||
print(f" - First blueprint: {sb.name} (ID: {sb.id})")
|
||||
print(f" - Pages: {sb.pages.count()}")
|
||||
|
||||
# 5. Check for Page Blueprints
|
||||
print("\n5. Checking for Page Blueprints...")
|
||||
pages = PageBlueprint.objects.filter(account=account)
|
||||
print(f" - Found {pages.count()} page blueprints")
|
||||
|
||||
if not pages.exists():
|
||||
print("❌ WARNING: No page blueprints found! Creating a test page...")
|
||||
|
||||
# Create a test site blueprint if needed
|
||||
if not site_blueprints.exists():
|
||||
from igny8_core.modules.planner.models import Sector
|
||||
sector = Sector.objects.filter(account=account).first()
|
||||
|
||||
if not sector:
|
||||
print("❌ ERROR: No sector found for account. Cannot create test blueprint.")
|
||||
return
|
||||
|
||||
sb = SiteBlueprint.objects.create(
|
||||
account=account,
|
||||
sector=sector,
|
||||
name="Test Site Blueprint",
|
||||
site_type="business",
|
||||
status="draft"
|
||||
)
|
||||
print(f"✓ Created test site blueprint: {sb.id}")
|
||||
else:
|
||||
sb = site_blueprints.first()
|
||||
|
||||
# Create a test page
|
||||
page = PageBlueprint.objects.create(
|
||||
account=account,
|
||||
site_blueprint=sb,
|
||||
sector=sb.sector,
|
||||
title="Test Home Page",
|
||||
slug="home",
|
||||
type="home",
|
||||
status="draft",
|
||||
blocks_json=[
|
||||
{
|
||||
"type": "hero",
|
||||
"heading": "Welcome to Our Test Site",
|
||||
"subheading": "This is a test page for content generation"
|
||||
},
|
||||
{
|
||||
"type": "features",
|
||||
"heading": "Our Features"
|
||||
}
|
||||
]
|
||||
)
|
||||
print(f"✓ Created test page blueprint: {page.id}")
|
||||
else:
|
||||
page = pages.first()
|
||||
print(f" - Using existing page: {page.title} (ID: {page.id})")
|
||||
|
||||
# 6. Test generate_page_content
|
||||
print("\n6. Testing generate_page_content...")
|
||||
print(f" - Page ID: {page.id}")
|
||||
print(f" - Page title: {page.title}")
|
||||
print(f" - Page type: {page.type}")
|
||||
print(f" - Page status: {page.status}")
|
||||
print(f" - Blocks count: {len(page.blocks_json) if page.blocks_json else 0}")
|
||||
|
||||
try:
|
||||
service = PageGenerationService()
|
||||
print("\n Calling service.generate_page_content()...")
|
||||
result = service.generate_page_content(page, force_regenerate=False)
|
||||
|
||||
print(f"\n✓ SUCCESS!")
|
||||
print(f" Result: {result}")
|
||||
|
||||
except ValueError as e:
|
||||
print(f"\n❌ ValueError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST COMPLETE")
|
||||
print("=" * 80)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test to check generate_content function execution
|
||||
Run this to see exact error messages
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
import logging
|
||||
import json
|
||||
|
||||
# Setup Django
|
||||
sys.path.insert(0, '/data/app/igny8/backend')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
# Setup logging to see all messages
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
||||
)
|
||||
|
||||
from igny8_core.auth.models import Account
|
||||
from igny8_core.modules.writer.models import Tasks
|
||||
from igny8_core.ai.registry import get_function_instance
|
||||
from igny8_core.ai.engine import AIEngine
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("SIMPLE GENERATE_CONTENT TEST")
|
||||
print("="*80 + "\n")
|
||||
|
||||
# Get first account and task
|
||||
try:
|
||||
account = Account.objects.first()
|
||||
print(f"✓ Account: {account.id} - {account.email}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to get account: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
task = Tasks.objects.filter(account=account).first()
|
||||
if not task:
|
||||
print("✗ No tasks found")
|
||||
sys.exit(1)
|
||||
print(f"✓ Task: {task.id} - {task.title or 'Untitled'}")
|
||||
print(f" Status: {task.status}")
|
||||
print(f" Cluster: {task.cluster.name if task.cluster else 'None'}")
|
||||
print(f" Taxonomy: {task.taxonomy_term.name if task.taxonomy_term else 'None'}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to get task: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Get function
|
||||
try:
|
||||
fn = get_function_instance('generate_content')
|
||||
print(f"✓ Function loaded: {fn.get_name()}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to load function: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Test validation
|
||||
print("\nTesting validation...")
|
||||
try:
|
||||
payload = {'ids': [task.id]}
|
||||
result = fn.validate(payload, account)
|
||||
if result['valid']:
|
||||
print(f"✓ Validation passed")
|
||||
else:
|
||||
print(f"✗ Validation failed: {result.get('error')}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"✗ Validation error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Test prepare
|
||||
print("\nTesting prepare...")
|
||||
try:
|
||||
data = fn.prepare(payload, account)
|
||||
print(f"✓ Prepare succeeded: {len(data) if isinstance(data, list) else 1} task(s)")
|
||||
except Exception as e:
|
||||
print(f"✗ Prepare error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Test build_prompt
|
||||
print("\nTesting build_prompt...")
|
||||
try:
|
||||
prompt = fn.build_prompt(data, account)
|
||||
print(f"✓ Prompt built: {len(prompt)} characters")
|
||||
print(f"\nPrompt preview (first 500 chars):")
|
||||
print("-" * 80)
|
||||
print(prompt[:500])
|
||||
print("-" * 80)
|
||||
except Exception as e:
|
||||
print(f"✗ Build prompt error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
# Test model config
|
||||
print("\nTesting model config...")
|
||||
try:
|
||||
from igny8_core.ai.settings import get_model_config
|
||||
model_config = get_model_config('generate_content', account=account)
|
||||
print(f"✓ Model config loaded:")
|
||||
print(f" Model: {model_config.get('model')}")
|
||||
print(f" Max tokens: {model_config.get('max_tokens')}")
|
||||
print(f" Temperature: {model_config.get('temperature')}")
|
||||
except Exception as e:
|
||||
print(f"✗ Model config error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
print("\n" + "="*80)
|
||||
print("All tests passed! The function structure is correct.")
|
||||
print("If content generation still fails, the issue is likely:")
|
||||
print("1. API key is invalid or missing")
|
||||
print("2. OpenAI API error (rate limit, quota, etc.)")
|
||||
print("3. Prompt is too long or has invalid format")
|
||||
print("4. Celery worker is not running or has errors")
|
||||
print("\nCheck Celery worker logs with:")
|
||||
print(" journalctl -u celery-worker -f")
|
||||
print("="*80 + "\n")
|
||||
@@ -1,162 +0,0 @@
|
||||
"""
|
||||
Test Script to Manually Push WordPress Structure to IGNY8 Backend
|
||||
This simulates what the WordPress plugin would send
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Configuration
|
||||
API_BASE = "https://api.igny8.com/api/v1"
|
||||
SITE_ID = 5 # From the URL: https://app.igny8.com/sites/5/settings
|
||||
|
||||
# You need to get an API key from Django admin or use your access token
|
||||
# For now, this script shows what data should be sent
|
||||
API_KEY = "YOUR_API_KEY_HERE" # Replace with actual API key
|
||||
|
||||
# Step 1: Get the integration ID for this WordPress site
|
||||
def get_integration_id():
|
||||
url = f"{API_BASE}/integration/integrations/"
|
||||
params = {"site": SITE_ID, "platform": "wordpress"}
|
||||
headers = {"Authorization": f"Bearer {API_KEY}"}
|
||||
|
||||
response = requests.get(url, params=params, headers=headers)
|
||||
print(f"Get Integration Response: {response.status_code}")
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
# Handle paginated response
|
||||
if 'results' in data:
|
||||
integrations = data['results']
|
||||
elif isinstance(data, list):
|
||||
integrations = data
|
||||
else:
|
||||
integrations = [data]
|
||||
|
||||
if integrations:
|
||||
return integrations[0]['id']
|
||||
|
||||
return None
|
||||
|
||||
# Step 2: Push WordPress structure to backend
|
||||
def push_structure(integration_id):
|
||||
url = f"{API_BASE}/integration/integrations/{integration_id}/update-structure/"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# Sample WordPress structure data
|
||||
payload = {
|
||||
"post_types": {
|
||||
"post": {
|
||||
"label": "Posts",
|
||||
"count": 150,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
},
|
||||
"page": {
|
||||
"label": "Pages",
|
||||
"count": 25,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
},
|
||||
"product": {
|
||||
"label": "Products",
|
||||
"count": 89,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
}
|
||||
},
|
||||
"taxonomies": {
|
||||
"category": {
|
||||
"label": "Categories",
|
||||
"count": 15,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
},
|
||||
"post_tag": {
|
||||
"label": "Tags",
|
||||
"count": 234,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
},
|
||||
"product_cat": {
|
||||
"label": "Product Categories",
|
||||
"count": 12,
|
||||
"enabled": True,
|
||||
"fetch_limit": 100
|
||||
}
|
||||
},
|
||||
"timestamp": "2025-11-22T04:20:00+00:00",
|
||||
"site_url": "https://example.com",
|
||||
"wordpress_version": "6.4",
|
||||
"plugin_connection_enabled": True,
|
||||
"two_way_sync_enabled": True
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
print(f"\nPush Structure Response: {response.status_code}")
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
# Step 3: Verify the data was stored
|
||||
def verify_content_types(integration_id):
|
||||
url = f"{API_BASE}/integration/integrations/{integration_id}/content-types/"
|
||||
headers = {"Authorization": f"Bearer {API_KEY}"}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
print(f"\nGet Content Types Response: {response.status_code}")
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
return response.status_code == 200
|
||||
|
||||
# Main execution
|
||||
if __name__ == "__main__":
|
||||
print("=== WordPress Structure Push Test ===\n")
|
||||
print(f"Site ID: {SITE_ID}")
|
||||
print(f"API Base: {API_BASE}\n")
|
||||
|
||||
if API_KEY == "YOUR_API_KEY_HERE":
|
||||
print("ERROR: Please set your API_KEY in the script!")
|
||||
print("\nTo get an API key:")
|
||||
print("1. Go to Django admin")
|
||||
print("2. Generate a WordPress API key for your site")
|
||||
print("3. Replace 'YOUR_API_KEY_HERE' in this script")
|
||||
exit(1)
|
||||
|
||||
# Step 1
|
||||
print("Step 1: Getting Integration ID...")
|
||||
integration_id = get_integration_id()
|
||||
|
||||
if not integration_id:
|
||||
print("\nERROR: Could not find WordPress integration for site!")
|
||||
print("Make sure:")
|
||||
print("1. The site exists in the database")
|
||||
print("2. A WordPress integration exists for this site")
|
||||
print("3. Your API key is valid")
|
||||
exit(1)
|
||||
|
||||
print(f"\n✓ Found Integration ID: {integration_id}")
|
||||
|
||||
# Step 2
|
||||
print("\nStep 2: Pushing WordPress structure...")
|
||||
if push_structure(integration_id):
|
||||
print("\n✓ Structure pushed successfully!")
|
||||
else:
|
||||
print("\n✗ Failed to push structure!")
|
||||
exit(1)
|
||||
|
||||
# Step 3
|
||||
print("\nStep 3: Verifying content types...")
|
||||
if verify_content_types(integration_id):
|
||||
print("\n✓ Content types verified!")
|
||||
print("\n=== TEST COMPLETE ===")
|
||||
print("\nNow refresh the frontend page:")
|
||||
print(f"https://app.igny8.com/sites/{SITE_ID}/settings?tab=content-types")
|
||||
else:
|
||||
print("\n✗ Could not verify content types!")
|
||||
exit(1)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Test generate_content for Writer tasks"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
|
||||
django.setup()
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from igny8_core.business.content.models import Tasks
|
||||
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
|
||||
|
||||
print("=" * 80)
|
||||
print("TESTING WRITER GENERATE_CONTENT")
|
||||
print("=" * 80)
|
||||
|
||||
User = get_user_model()
|
||||
user = User.objects.filter(email='dev@igny8.com').first()
|
||||
|
||||
if not user:
|
||||
print("❌ User not found")
|
||||
sys.exit(1)
|
||||
|
||||
account = user.account if hasattr(user, 'account') else None
|
||||
|
||||
if not account:
|
||||
print("❌ No account found for user")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✓ User: {user.email}")
|
||||
print(f"✓ Account: {account.id}")
|
||||
|
||||
# Get tasks
|
||||
tasks = Tasks.objects.filter(account=account, status='queued')[:5]
|
||||
print(f"\n✓ Found {tasks.count()} queued tasks")
|
||||
|
||||
if tasks.exists():
|
||||
task = tasks.first()
|
||||
print(f"\nTesting with task:")
|
||||
print(f" - ID: {task.id}")
|
||||
print(f" - Title: {task.title}")
|
||||
print(f" - Status: {task.status}")
|
||||
print(f" - Cluster: {task.cluster.name if task.cluster else 'None'}")
|
||||
|
||||
print("\nCalling generate_content service...")
|
||||
service = ContentGenerationService()
|
||||
|
||||
try:
|
||||
result = service.generate_content([task.id], account)
|
||||
print(f"\n✓ SUCCESS!")
|
||||
print(f"Result: {result}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
else:
|
||||
print("\n⚠️ No queued tasks found. Creating a test task...")
|
||||
|
||||
from igny8_core.modules.planner.models import Clusters
|
||||
cluster = Clusters.objects.filter(account=account).first()
|
||||
|
||||
if not cluster:
|
||||
print("❌ No clusters found. Cannot create test task.")
|
||||
sys.exit(1)
|
||||
|
||||
task = Tasks.objects.create(
|
||||
account=account,
|
||||
site=cluster.site,
|
||||
sector=cluster.sector,
|
||||
cluster=cluster,
|
||||
title="Test Article: Benefits of Organic Cotton Bedding",
|
||||
description="Comprehensive guide covering health benefits, environmental impact, and buying guide",
|
||||
content_type='post',
|
||||
content_structure='article',
|
||||
status='queued'
|
||||
)
|
||||
print(f"✓ Created test task: {task.id} - {task.title}")
|
||||
|
||||
print("\nCalling generate_content service...")
|
||||
service = ContentGenerationService()
|
||||
|
||||
try:
|
||||
result = service.generate_content([task.id], account)
|
||||
print(f"\n✓ SUCCESS!")
|
||||
print(f"Result: {result}")
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("TEST COMPLETE")
|
||||
print("=" * 80)
|
||||
Reference in New Issue
Block a user