Stage 1 migration and docs complete
This commit is contained in:
@@ -1,278 +0,0 @@
|
||||
# 🎉 STAGE 1 BACKEND REFACTOR - COMPLETION SUMMARY
|
||||
|
||||
**Date:** November 24, 2025
|
||||
**Status:** ✅ **COMPLETE**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Overview
|
||||
|
||||
All Stage 1 work items have been successfully completed per the STAGE 1 COMPLETION PROMPT requirements.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Completed Work Items
|
||||
|
||||
### Part A: Confirmed Completed Work ✅
|
||||
- [x] **Cluster Model** - Removed `context_type` and `dimension_meta` fields
|
||||
- [x] **Task Model** - Removed 7 fields, added 3 new fields, simplified status
|
||||
- [x] **Content Model** - Removed 25+ fields, added 5 new fields, simplified status
|
||||
- [x] **ContentTaxonomy Model** - Removed 6 fields, added 'cluster' taxonomy type
|
||||
|
||||
**Files Modified:**
|
||||
- `backend/igny8_core/business/planning/models.py`
|
||||
- `backend/igny8_core/business/content/models.py`
|
||||
|
||||
---
|
||||
|
||||
### Part B: Serializers Refactored ✅
|
||||
|
||||
#### TasksSerializer
|
||||
**File:** `backend/igny8_core/modules/writer/serializers.py`
|
||||
|
||||
**Changes Made:**
|
||||
- ✅ Removed deprecated imports (ContentIdeas, ContentClusterMap, ContentTaxonomyMap, ContentAttribute)
|
||||
- ✅ Updated fields list: `cluster_id`, `content_type`, `content_structure`, `taxonomy_term_id`, `status`
|
||||
- ✅ Removed deprecated methods: `get_idea_title`, `_get_content_record`, `get_content_html`, `get_cluster_mappings`, `get_taxonomy_mappings`, `get_attribute_mappings`
|
||||
- ✅ Added validation: require `cluster`, `content_type`, `content_structure` on create
|
||||
|
||||
#### ContentSerializer
|
||||
**File:** `backend/igny8_core/modules/writer/serializers.py`
|
||||
|
||||
**Changes Made:**
|
||||
- ✅ Updated fields list: `id`, `title`, `content_html`, `cluster_id`, `cluster_name`, `content_type`, `content_structure`, `taxonomy_terms_data`, `external_id`, `external_url`, `source`, `status`
|
||||
- ✅ Removed deprecated fields: `task_id`, `html_content`, `entity_type`, `cluster_role`, `sync_status`, etc.
|
||||
- ✅ Added methods: `get_cluster_name()`, `get_sector_name()`, `get_taxonomy_terms_data()`
|
||||
- ✅ Added validation: require `cluster`, `content_type`, `content_structure`, `title` on create
|
||||
- ✅ Set defaults: `source='igny8'`, `status='draft'`
|
||||
|
||||
#### ContentTaxonomySerializer
|
||||
**File:** `backend/igny8_core/modules/writer/serializers.py`
|
||||
|
||||
**Changes Made:**
|
||||
- ✅ Updated fields list: `id`, `name`, `slug`, `taxonomy_type`, `external_id`, `external_taxonomy`, `content_count`
|
||||
- ✅ Removed deprecated fields: `description`, `parent`, `parent_name`, `sync_status`, `count`, `metadata`, `cluster_names`
|
||||
- ✅ Removed methods: `get_parent_name()`, `get_cluster_names()`
|
||||
- ✅ Kept method: `get_content_count()`
|
||||
|
||||
#### Deprecated Serializers Removed
|
||||
- ✅ `ContentAttributeSerializer` - Model removed
|
||||
- ✅ `ContentTaxonomyRelationSerializer` - Through model removed
|
||||
- ✅ `UpdatedTasksSerializer` - Duplicate removed
|
||||
|
||||
---
|
||||
|
||||
### Part C: API Endpoints & ViewSets Updated ✅
|
||||
|
||||
#### TasksViewSet
|
||||
**File:** `backend/igny8_core/modules/writer/views.py`
|
||||
|
||||
**Changes Made:**
|
||||
- ✅ Updated queryset: `select_related('cluster', 'site', 'sector')` (removed `content_record`)
|
||||
- ✅ Updated filters: `['status', 'cluster_id', 'content_type', 'content_structure']`
|
||||
- ✅ Removed deprecated filters: `entity_type`, `cluster_role`
|
||||
|
||||
#### ContentViewSet
|
||||
**File:** `backend/igny8_core/modules/writer/views.py`
|
||||
|
||||
**Changes Made:**
|
||||
- ✅ Updated queryset: `select_related('cluster', 'site', 'sector').prefetch_related('taxonomy_terms')`
|
||||
- ✅ Updated search fields: `['title', 'content_html', 'external_url']`
|
||||
- ✅ Updated filters: `['cluster_id', 'status', 'content_type', 'content_structure', 'source']`
|
||||
- ✅ Removed deprecated filters: `task_id`, `entity_type`, `content_format`, `cluster_role`, `sync_status`, `external_type`
|
||||
- ✅ **Publish endpoint scaffolded** - See `backend/STAGE_1_PUBLISH_ENDPOINT.py` for implementation
|
||||
|
||||
**Note:** The `publish()` endpoint code is ready in `STAGE_1_PUBLISH_ENDPOINT.py`. It needs to be manually inserted into `ContentViewSet` after line 903 (after the `validate()` method).
|
||||
|
||||
#### Views Imports Updated
|
||||
- ✅ Removed `ContentAttributeSerializer` import (model deleted)
|
||||
- ✅ Removed `ContentAttribute` model import
|
||||
|
||||
---
|
||||
|
||||
### Part D: WordPress Import/Publish Services ⚠️
|
||||
|
||||
**Status:** Placeholder implementation created
|
||||
|
||||
**File:** `backend/STAGE_1_PUBLISH_ENDPOINT.py`
|
||||
|
||||
**Implementation:**
|
||||
- ✅ Endpoint scaffolded: `POST /api/v1/writer/content/{id}/publish/`
|
||||
- ✅ Builds WordPress API payload with meta fields
|
||||
- ✅ Maps taxonomy terms to WP categories/tags
|
||||
- ✅ Updates `external_id`, `external_url`, `status='published'`
|
||||
- ⚠️ **TODO:** Add real WordPress REST API authentication (currently placeholder)
|
||||
|
||||
**Next Steps:**
|
||||
1. Get WordPress credentials from `site.metadata` or environment
|
||||
2. Implement actual `requests.post()` call to WP REST API
|
||||
3. Handle WP authentication (Application Password or OAuth)
|
||||
4. Add error handling for WP API failures
|
||||
|
||||
---
|
||||
|
||||
### Part E: Migrations Generated ✅
|
||||
|
||||
**Files Created:**
|
||||
1. `backend/igny8_core/business/planning/migrations/0002_stage1_remove_cluster_context_fields.py`
|
||||
2. `backend/igny8_core/business/content/migrations/0002_stage1_refactor_task_content_taxonomy.py`
|
||||
|
||||
**Migration Script:** `backend/STAGE_1_RUN_MIGRATIONS.ps1`
|
||||
|
||||
**To Run Migrations:**
|
||||
```powershell
|
||||
cd backend
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# Backup database first!
|
||||
# pg_dump -U your_user -d your_database > backup_stage1_$(Get-Date -Format 'yyyyMMdd_HHmmss').sql
|
||||
|
||||
# Apply migrations
|
||||
python manage.py migrate planning 0002_stage1_remove_cluster_context_fields
|
||||
python manage.py migrate content 0002_stage1_refactor_task_content_taxonomy
|
||||
|
||||
# Verify
|
||||
python manage.py check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Part F: Basic Tests Created ✅
|
||||
|
||||
**File:** `backend/igny8_core/modules/writer/tests/test_stage1_refactor.py`
|
||||
|
||||
**Test Coverage:**
|
||||
- ✅ `TestClusterModel` - Verifies removed/existing fields
|
||||
- ✅ `TestTasksModel` - Verifies removed/added fields, status choices
|
||||
- ✅ `TestContentModel` - Verifies removed/added fields, status choices, M2M relationship
|
||||
- ✅ `TestContentTaxonomyModel` - Verifies removed fields, taxonomy_type choices
|
||||
- ✅ `TestTasksSerializer` - Verifies serializer fields
|
||||
- ✅ `TestContentSerializer` - Verifies serializer fields
|
||||
- ✅ `TestContentTaxonomySerializer` - Verifies serializer fields
|
||||
|
||||
**To Run Tests:**
|
||||
```powershell
|
||||
cd backend
|
||||
python manage.py test igny8_core.modules.writer.tests.test_stage1_refactor
|
||||
# Or with pytest:
|
||||
pytest backend/igny8_core/modules/writer/tests/test_stage1_refactor.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Part G: MASTER_REFERENCE.md Updated ✅
|
||||
|
||||
**File:** `MASTER_REFERENCE.md`
|
||||
|
||||
**Updates Made:**
|
||||
- ✅ Updated Cluster model documentation (removed `context_type`, `dimension_meta`)
|
||||
- ✅ Updated Task model documentation (removed 7 fields, added 3 fields, status changes)
|
||||
- ✅ Updated Content model documentation (removed 25+ fields, added 5 fields, status changes)
|
||||
- ✅ Added ContentTaxonomy model documentation (removed 6 fields, added 'cluster' type)
|
||||
- ✅ Added Stage 1 change notes to each model
|
||||
|
||||
---
|
||||
|
||||
### Part H: CHANGELOG.md Updated ✅
|
||||
|
||||
**File:** `CHANGELOG.md`
|
||||
|
||||
**Updates Made:**
|
||||
- ✅ Added "STAGE 1 COMPLETE" section at top of v1.0.0 entry
|
||||
- ✅ Listed all completed work items
|
||||
- ✅ Referenced migration files and test files
|
||||
- ✅ Added migration commands reference
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files Created/Modified Summary
|
||||
|
||||
### New Files Created
|
||||
1. `backend/igny8_core/business/planning/migrations/0002_stage1_remove_cluster_context_fields.py`
|
||||
2. `backend/igny8_core/business/content/migrations/0002_stage1_refactor_task_content_taxonomy.py`
|
||||
3. `backend/igny8_core/modules/writer/tests/test_stage1_refactor.py`
|
||||
4. `backend/STAGE_1_PUBLISH_ENDPOINT.py` (code snippet for manual insertion)
|
||||
5. `backend/STAGE_1_RUN_MIGRATIONS.ps1` (PowerShell migration script)
|
||||
6. `backend/STAGE_1_COMPLETION_SUMMARY.md` (this file)
|
||||
|
||||
### Files Modified
|
||||
1. `backend/igny8_core/business/planning/models.py` - Cluster model refactored
|
||||
2. `backend/igny8_core/business/content/models.py` - Task, Content, ContentTaxonomy refactored
|
||||
3. `backend/igny8_core/modules/writer/serializers.py` - 3 serializers refactored, 3 removed
|
||||
4. `backend/igny8_core/modules/writer/views.py` - TasksViewSet and ContentViewSet updated
|
||||
5. `MASTER_REFERENCE.md` - Data Models section updated
|
||||
6. `CHANGELOG.md` - Stage 1 completion note added
|
||||
7. `backend/STAGE_1_EXECUTION_REPORT.md` - Previously created
|
||||
8. `backend/STAGE_1_REFACTOR_COMPLETE_SUMMARY.md` - Previously created
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Next Steps to Deploy Stage 1
|
||||
|
||||
### 1. Manual Code Insertion Required
|
||||
- **File:** `backend/igny8_core/modules/writer/views.py`
|
||||
- **Action:** Insert the `publish()` method from `STAGE_1_PUBLISH_ENDPOINT.py` into `ContentViewSet` class after line 903
|
||||
- **Reason:** Could not auto-insert due to multiple similar methods in file
|
||||
|
||||
### 2. Run Migrations
|
||||
```powershell
|
||||
cd backend
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# Backup database
|
||||
pg_dump -U your_user -d your_database > backup_stage1.sql
|
||||
|
||||
# Run migrations
|
||||
python manage.py migrate planning 0002
|
||||
python manage.py migrate content 0002
|
||||
|
||||
# Verify
|
||||
python manage.py check
|
||||
```
|
||||
|
||||
### 3. Run Tests
|
||||
```powershell
|
||||
python manage.py test igny8_core.modules.writer.tests.test_stage1_refactor
|
||||
```
|
||||
|
||||
### 4. Update Existing Data (if needed)
|
||||
- Tasks with old status values need migration to 'queued' or 'completed'
|
||||
- Content with old status values need migration to 'draft' or 'published'
|
||||
- Consider creating a data migration script if needed
|
||||
|
||||
### 5. Complete WordPress Publish Integration
|
||||
- Implement real WP REST API authentication in `publish()` endpoint
|
||||
- Get credentials from `Site.metadata` or environment variables
|
||||
- Test publish workflow with real WordPress site
|
||||
|
||||
---
|
||||
|
||||
## ✅ Stage 1 Checklist (ALL COMPLETE)
|
||||
|
||||
- [x] Part A: Confirm models refactored
|
||||
- [x] Part B: Complete serializers (TasksSerializer, ContentSerializer, ContentTaxonomySerializer)
|
||||
- [x] Part C: Complete API endpoints (TasksViewSet, ContentViewSet filters, publish endpoint scaffolded)
|
||||
- [x] Part D: WordPress import/publish service scaffolded
|
||||
- [x] Part E: Generate and document migrations
|
||||
- [x] Part F: Add basic tests
|
||||
- [x] Part G: Update MASTER_REFERENCE.md
|
||||
- [x] Part H: Update CHANGELOG.md
|
||||
- [x] Final Summary: This document
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Final Notes
|
||||
|
||||
**Stage 1 is architecturally complete.** All code changes, migrations, tests, and documentation are ready.
|
||||
|
||||
**Remaining Work:**
|
||||
1. Manually insert publish endpoint (5 minutes)
|
||||
2. Run migrations (5 minutes)
|
||||
3. Run tests to verify (5 minutes)
|
||||
4. Complete WordPress API authentication (30-60 minutes)
|
||||
|
||||
**Total Time to Production:** ~1-2 hours
|
||||
|
||||
---
|
||||
|
||||
**Stage 1 Complete!** 🎉
|
||||
@@ -1,98 +0,0 @@
|
||||
# Stage 1: Content Publish Endpoint
|
||||
# This code should be inserted into writer/views.py ContentViewSet class
|
||||
# Insert after the validate() method (around line 903)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='publish', url_name='publish', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
|
||||
def publish(self, request, pk=None):
|
||||
"""
|
||||
Stage 1: Publish content to WordPress site.
|
||||
|
||||
POST /api/v1/writer/content/{id}/publish/
|
||||
{
|
||||
"site_id": 1 // WordPress site to publish to
|
||||
}
|
||||
"""
|
||||
import requests
|
||||
from igny8_core.auth.models import Site
|
||||
|
||||
content = self.get_object()
|
||||
site_id = request.data.get('site_id')
|
||||
|
||||
if not site_id:
|
||||
return error_response(
|
||||
error='site_id is required',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
try:
|
||||
site = Site.objects.get(id=site_id)
|
||||
except Site.DoesNotExist:
|
||||
return error_response(
|
||||
error=f'Site with id {site_id} does not exist',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Build WordPress API payload
|
||||
wp_payload = {
|
||||
'title': content.title,
|
||||
'content': content.content_html,
|
||||
'status': 'publish', # or 'draft' based on content.status
|
||||
'meta': {
|
||||
'_igny8_content_id': str(content.id),
|
||||
'_igny8_cluster_id': str(content.cluster_id) if content.cluster_id else '',
|
||||
'_igny8_content_type': content.content_type,
|
||||
'_igny8_content_structure': content.content_structure,
|
||||
},
|
||||
}
|
||||
|
||||
# Add taxonomy terms if present
|
||||
if content.taxonomy_terms.exists():
|
||||
wp_categories = []
|
||||
wp_tags = []
|
||||
for term in content.taxonomy_terms.all():
|
||||
if term.taxonomy_type == 'category' and term.external_id:
|
||||
wp_categories.append(int(term.external_id))
|
||||
elif term.taxonomy_type == 'post_tag' and term.external_id:
|
||||
wp_tags.append(int(term.external_id))
|
||||
|
||||
if wp_categories:
|
||||
wp_payload['categories'] = wp_categories
|
||||
if wp_tags:
|
||||
wp_payload['tags'] = wp_tags
|
||||
|
||||
# Call WordPress REST API (using site's WP credentials)
|
||||
try:
|
||||
# TODO: Get WP credentials from site.metadata or environment
|
||||
wp_url = site.url # Assuming site.url is the WordPress URL
|
||||
wp_endpoint = f'{wp_url}/wp-json/wp/v2/posts'
|
||||
|
||||
# This is a placeholder - real implementation needs proper auth
|
||||
# response = requests.post(wp_endpoint, json=wp_payload, auth=(wp_user, wp_password))
|
||||
# response.raise_for_status()
|
||||
# wp_post_data = response.json()
|
||||
|
||||
# For now, just mark as published and return success
|
||||
content.status = 'published'
|
||||
content.external_id = '12345' # Would be: str(wp_post_data['id'])
|
||||
content.external_url = f'{wp_url}/?p=12345' # Would be: wp_post_data['link']
|
||||
content.save()
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'content_id': content.id,
|
||||
'status': content.status,
|
||||
'external_id': content.external_id,
|
||||
'external_url': content.external_url,
|
||||
'message': 'Content published to WordPress (placeholder implementation)',
|
||||
},
|
||||
message='Content published successfully',
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
return error_response(
|
||||
error=f'Failed to publish to WordPress: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
@@ -1,121 +0,0 @@
|
||||
# 🚀 STAGE 1 - QUICK START GUIDE
|
||||
|
||||
## ⚡ Fast Track to Production (15 minutes)
|
||||
|
||||
### Step 1: Insert Publish Endpoint (5 min)
|
||||
```powershell
|
||||
# Open file
|
||||
code backend/igny8_core/modules/writer/views.py
|
||||
|
||||
# Find line 903 (after validate() method in ContentViewSet)
|
||||
# Copy code from: backend/STAGE_1_PUBLISH_ENDPOINT.py
|
||||
# Paste after line 903
|
||||
```
|
||||
|
||||
### Step 2: Run Migrations (5 min)
|
||||
```powershell
|
||||
cd backend
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# Backup database first!
|
||||
pg_dump -U your_user -d igny8_db > backup_stage1.sql
|
||||
|
||||
# Run migrations
|
||||
python manage.py migrate planning 0002_stage1_remove_cluster_context_fields
|
||||
python manage.py migrate content 0002_stage1_refactor_task_content_taxonomy
|
||||
|
||||
# Verify
|
||||
python manage.py check
|
||||
```
|
||||
|
||||
### Step 3: Run Tests (5 min)
|
||||
```powershell
|
||||
python manage.py test igny8_core.modules.writer.tests.test_stage1_refactor -v
|
||||
```
|
||||
|
||||
### ✅ Done!
|
||||
Your Stage 1 refactor is now live.
|
||||
|
||||
---
|
||||
|
||||
## 📋 What Changed (TL;DR)
|
||||
|
||||
### Models Simplified
|
||||
- **Cluster:** Removed multi-dimensional metadata → Pure topic clusters
|
||||
- **Task:** Removed 7 fields, added 3 → Now content-type focused
|
||||
- **Content:** Removed 25+ fields, added 5 → Simplified publishing model
|
||||
- **ContentTaxonomy:** Removed 6 fields → Essential fields only
|
||||
|
||||
### Status Simplified
|
||||
- **Task:** `queued` → `completed` (was 4 states)
|
||||
- **Content:** `draft` → `published` (was 4 states)
|
||||
|
||||
### API Changes
|
||||
- **Filters:** Removed deprecated fields (entity_type, cluster_role, sync_status)
|
||||
- **New Fields:** content_type, content_structure
|
||||
- **New Endpoint:** POST /api/v1/writer/content/{id}/publish/
|
||||
|
||||
---
|
||||
|
||||
## 📁 Key Files
|
||||
|
||||
### Migration Files
|
||||
- `planning/migrations/0002_stage1_remove_cluster_context_fields.py`
|
||||
- `content/migrations/0002_stage1_refactor_task_content_taxonomy.py`
|
||||
|
||||
### Test File
|
||||
- `igny8_core/modules/writer/tests/test_stage1_refactor.py`
|
||||
|
||||
### Documentation
|
||||
- `STAGE_1_COMPLETION_SUMMARY.md` - Full completion report
|
||||
- `STAGE_1_EXECUTION_REPORT.md` - Detailed before/after
|
||||
- `MASTER_REFERENCE.md` - Updated model docs
|
||||
- `CHANGELOG.md` - Version history
|
||||
|
||||
---
|
||||
|
||||
## 🔧 If Something Breaks
|
||||
|
||||
### Rollback Migrations
|
||||
```powershell
|
||||
python manage.py migrate planning 0001_initial
|
||||
python manage.py migrate content 0001_initial
|
||||
```
|
||||
|
||||
### Restore Database
|
||||
```powershell
|
||||
psql -U your_user -d igny8_db < backup_stage1.sql
|
||||
```
|
||||
|
||||
### Check Errors
|
||||
```powershell
|
||||
python manage.py check
|
||||
python manage.py showmigrations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Next: Complete WordPress Integration
|
||||
|
||||
Edit `backend/igny8_core/modules/writer/views.py` line ~950:
|
||||
|
||||
```python
|
||||
# In publish() method, replace placeholder with:
|
||||
wp_user = site.metadata.get('wp_username')
|
||||
wp_password = site.metadata.get('wp_app_password')
|
||||
|
||||
response = requests.post(
|
||||
wp_endpoint,
|
||||
json=wp_payload,
|
||||
auth=(wp_user, wp_password)
|
||||
)
|
||||
response.raise_for_status()
|
||||
wp_post_data = response.json()
|
||||
|
||||
content.external_id = str(wp_post_data['id'])
|
||||
content.external_url = wp_post_data['link']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Questions?** See `STAGE_1_COMPLETION_SUMMARY.md` for full details.
|
||||
@@ -1,47 +0,0 @@
|
||||
# Stage 1 Migration Commands
|
||||
# Run these commands in order after activating your Python virtual environment
|
||||
|
||||
# Navigate to backend directory
|
||||
cd backend
|
||||
|
||||
# Activate virtual environment (adjust path if needed)
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
|
||||
# IMPORTANT: Backup your database first!
|
||||
# For PostgreSQL:
|
||||
# pg_dump -U your_user -d your_database > backup_stage1_$(Get-Date -Format 'yyyyMMdd_HHmmss').sql
|
||||
|
||||
# For SQLite (if using for dev):
|
||||
# Copy-Item db.sqlite3 "db_backup_stage1_$(Get-Date -Format 'yyyyMMdd_HHmmss').sqlite3"
|
||||
|
||||
# Check migration status
|
||||
python manage.py showmigrations planning
|
||||
python manage.py showmigrations content
|
||||
|
||||
# Apply the migrations
|
||||
python manage.py migrate planning 0002_stage1_remove_cluster_context_fields
|
||||
python manage.py migrate content 0002_stage1_refactor_task_content_taxonomy
|
||||
|
||||
# Verify migrations were applied
|
||||
python manage.py showmigrations planning
|
||||
python manage.py showmigrations content
|
||||
|
||||
# Check for any migration conflicts
|
||||
python manage.py makemigrations --check --dry-run
|
||||
|
||||
# Test that Django can load the models
|
||||
python manage.py check
|
||||
|
||||
# Optional: Open Django shell to verify model changes
|
||||
# python manage.py shell
|
||||
# >>> from igny8_core.business.planning.models import Clusters
|
||||
# >>> from igny8_core.business.content.models import Tasks, Content, ContentTaxonomy
|
||||
# >>> print(Tasks._meta.get_fields())
|
||||
# >>> print(Content._meta.get_fields())
|
||||
# >>> print(ContentTaxonomy._meta.get_fields())
|
||||
|
||||
# If you encounter issues, you can rollback:
|
||||
# python manage.py migrate planning 0001_initial
|
||||
# python manage.py migrate content 0001_initial
|
||||
|
||||
Write-Host "Migration commands completed!" -ForegroundColor Green
|
||||
Binary file not shown.
@@ -103,6 +103,7 @@ class Content(SiteSectorBaseModel):
|
||||
'ContentTaxonomy',
|
||||
blank=True,
|
||||
related_name='contents',
|
||||
db_table='igny8_content_taxonomy_relations',
|
||||
help_text="Associated taxonomy terms (categories, tags, attributes)"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-25 15:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('planner', '0003_cleanup_remove_deprecated_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='clusters',
|
||||
name='igny8_clust_context_0d6bd7_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='contentideas',
|
||||
name='igny8_conte_content_3eede7_idx',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='clusters',
|
||||
name='context_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='clusters',
|
||||
name='dimension_meta',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contentideas',
|
||||
name='cluster_role',
|
||||
field=models.CharField(choices=[('hub', 'Hub'), ('supporting', 'Supporting'), ('attribute', 'Attribute')], default='hub', help_text='Role within the cluster-driven sitemap', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contentideas',
|
||||
name='site_entity_type',
|
||||
field=models.CharField(choices=[('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('service', 'Service'), ('taxonomy_term', 'Taxonomy Term')], default='page', help_text='Target entity type when promoting idea into tasks/pages', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -6,9 +6,9 @@ from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute
|
||||
|
||||
@admin.register(Tasks)
|
||||
class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
list_display = ['title', 'entity_type', 'cluster_role', 'site', 'sector', 'status', 'cluster', 'created_at']
|
||||
list_filter = ['status', 'entity_type', 'cluster_role', 'site', 'sector', 'cluster']
|
||||
search_fields = ['title', 'description', 'keywords']
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'status', 'cluster', 'created_at']
|
||||
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'cluster']
|
||||
search_fields = ['title', 'description']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
@@ -17,10 +17,10 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
'fields': ('title', 'description', 'status', 'site', 'sector')
|
||||
}),
|
||||
('Content Classification', {
|
||||
'fields': ('entity_type', 'cluster_role', 'taxonomy')
|
||||
'fields': ('content_type', 'content_structure', 'taxonomy_term')
|
||||
}),
|
||||
('Planning', {
|
||||
'fields': ('cluster', 'idea', 'keywords')
|
||||
'fields': ('cluster', 'keywords')
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
@@ -56,7 +56,7 @@ class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
list_display = ['get_content_title', 'site', 'sector', 'image_type', 'status', 'position', 'created_at']
|
||||
list_filter = ['image_type', 'status', 'site', 'sector']
|
||||
search_fields = ['content__title', 'content__meta_title', 'task__title']
|
||||
search_fields = ['content__title']
|
||||
ordering = ['-id'] # Sort by ID descending (newest first)
|
||||
|
||||
def get_content_title(self, obj):
|
||||
@@ -86,35 +86,32 @@ class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
|
||||
@admin.register(Content)
|
||||
class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
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']
|
||||
search_fields = ['title', 'meta_title', 'primary_keyword', 'task__title', 'external_url']
|
||||
ordering = ['-generated_at']
|
||||
readonly_fields = ['generated_at', 'updated_at']
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'created_at']
|
||||
list_filter = ['content_type', 'content_structure', 'source', 'status', 'site', 'sector', 'created_at']
|
||||
search_fields = ['title', 'content_html', 'external_url']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('title', 'task', 'site', 'sector', 'cluster', 'status')
|
||||
'fields': ('title', 'site', 'sector', 'cluster', 'status')
|
||||
}),
|
||||
('Content Classification', {
|
||||
'fields': ('entity_type', 'content_format', 'cluster_role', 'external_type')
|
||||
'fields': ('content_type', 'content_structure', 'source')
|
||||
}),
|
||||
('Content', {
|
||||
'fields': ('html_content', 'word_count', 'json_blocks', 'structure_data')
|
||||
'fields': ('content_html',)
|
||||
}),
|
||||
('SEO', {
|
||||
'fields': ('meta_title', 'meta_description', 'primary_keyword', 'secondary_keywords')
|
||||
('Taxonomy', {
|
||||
'fields': ('taxonomy_terms',),
|
||||
'description': 'Categories, tags, and other taxonomy terms'
|
||||
}),
|
||||
('WordPress Sync', {
|
||||
'fields': ('source', 'sync_status', 'external_id', 'external_url', 'sync_metadata'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Optimization', {
|
||||
'fields': ('linker_version', 'optimizer_version', 'optimization_scores', 'internal_links'),
|
||||
'fields': ('external_id', 'external_url'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('generated_at', 'updated_at'),
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
@@ -137,32 +134,23 @@ class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
|
||||
@admin.register(ContentTaxonomy)
|
||||
class ContentTaxonomyAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
|
||||
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']
|
||||
search_fields = ['name', 'slug', 'description', 'external_taxonomy']
|
||||
list_display = ['name', 'taxonomy_type', 'slug', 'external_id', 'external_taxonomy', 'site', 'sector']
|
||||
list_filter = ['taxonomy_type', 'site', 'sector']
|
||||
search_fields = ['name', 'slug', 'external_taxonomy']
|
||||
ordering = ['taxonomy_type', 'name']
|
||||
filter_horizontal = ['clusters']
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('name', 'slug', 'taxonomy_type', 'description', 'site', 'sector')
|
||||
}),
|
||||
('Hierarchy', {
|
||||
'fields': ('parent',),
|
||||
'description': 'Set parent for hierarchical taxonomies (categories).'
|
||||
'fields': ('name', 'slug', 'taxonomy_type', 'site', 'sector')
|
||||
}),
|
||||
('WordPress Sync', {
|
||||
'fields': ('external_id', 'external_taxonomy', 'sync_status', 'count', 'metadata')
|
||||
}),
|
||||
('Semantic Mapping', {
|
||||
'fields': ('clusters',),
|
||||
'description': 'Map this taxonomy to semantic clusters for AI optimization.'
|
||||
'fields': ('external_id', 'external_taxonomy')
|
||||
}),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super().get_queryset(request)
|
||||
return qs.select_related('parent', 'site', 'sector').prefetch_related('clusters')
|
||||
return qs.select_related('site', 'sector')
|
||||
|
||||
|
||||
@admin.register(ContentAttribute)
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-25 15:59
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_auth', '0002_add_wp_api_key_to_site'),
|
||||
('planner', '0004_remove_clusters_igny8_clust_context_0d6bd7_idx_and_more'),
|
||||
('writer', '0006_cleanup_migrate_and_drop_deprecated_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='contenttaxonomyrelation',
|
||||
unique_together=None,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomyrelation',
|
||||
name='content',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomyrelation',
|
||||
name='taxonomy',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='taxonomies',
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='content',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Content', 'verbose_name_plural': 'Contents'},
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_task_id_712988_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_generat_7128df_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_sync_st_02d5bd_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_source_df78d0_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_entity__f559b3_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_content_b538ee_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_cluster_32e22a_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_externa_a26125_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='content',
|
||||
name='igny8_conte_site_id_e559d5_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='contenttaxonomy',
|
||||
name='igny8_conte_sync_st_307b43_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='tasks',
|
||||
name='igny8_tasks_entity__1dc185_idx',
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name='tasks',
|
||||
name='igny8_tasks_cluster_c87903_idx',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='cluster_role',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='content_format',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='entity_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='external_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='generated_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='html_content',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='internal_links',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='json_blocks',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='linker_version',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='meta_description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='meta_title',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='metadata',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='optimization_scores',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='optimizer_version',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='primary_keyword',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='secondary_keywords',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='structure_data',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='sync_metadata',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='sync_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='task',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='content',
|
||||
name='word_count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='clusters',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='metadata',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='parent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='contenttaxonomy',
|
||||
name='sync_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='cluster_role',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='entity_type',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='idea',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='keyword_objects',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='taxonomy',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='content',
|
||||
name='content_html',
|
||||
field=models.TextField(default='', help_text='Final HTML content'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='content',
|
||||
name='content_structure',
|
||||
field=models.CharField(db_index=True, default='post', help_text='Content structure/format: article, listicle, guide, comparison, product_page, etc.', max_length=100),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='content',
|
||||
name='content_type',
|
||||
field=models.CharField(db_index=True, default='article', help_text='Content type: post, page, product, service, category, tag, etc.', max_length=100),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='content',
|
||||
name='taxonomy_terms',
|
||||
field=models.ManyToManyField(blank=True, db_table='igny8_content_taxonomy_relations', help_text='Associated taxonomy terms (categories, tags, attributes)', related_name='contents', to='writer.contenttaxonomy'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='content_structure',
|
||||
field=models.CharField(db_index=True, default='', help_text='Content structure/format: article, listicle, guide, comparison, product_page, etc.', max_length=100),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='content_type',
|
||||
field=models.CharField(db_index=True, default='post', help_text='Content type: post, page, product, service, category, tag, etc.', max_length=100),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='taxonomy_term',
|
||||
field=models.ForeignKey(blank=True, help_text='Optional taxonomy term assignment', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='writer.contenttaxonomy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(help_text='Parent cluster (required)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contents', to='planner.clusters'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, db_index=True, help_text='WordPress/external platform post ID', max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='external_url',
|
||||
field=models.URLField(blank=True, help_text='WordPress/external platform URL', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='source',
|
||||
field=models.CharField(choices=[('igny8', 'IGNY8 Generated'), ('wordpress', 'WordPress Imported')], db_index=True, default='igny8', help_text='Content source', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], db_index=True, default='draft', help_text='Content status', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='content',
|
||||
name='title',
|
||||
field=models.CharField(db_index=True, default='article', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contenttaxonomy',
|
||||
name='external_id',
|
||||
field=models.IntegerField(blank=True, db_index=True, help_text='WordPress term_id - null for cluster taxonomies', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contenttaxonomy',
|
||||
name='external_taxonomy',
|
||||
field=models.CharField(blank=True, help_text='WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - null for cluster taxonomies', max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='contenttaxonomy',
|
||||
name='taxonomy_type',
|
||||
field=models.CharField(choices=[('category', 'Category'), ('tag', 'Tag'), ('product_category', 'Product Category'), ('product_attribute', 'Product Attribute'), ('cluster', 'Cluster Taxonomy')], db_index=True, help_text='Type of taxonomy', max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tasks',
|
||||
name='cluster',
|
||||
field=models.ForeignKey(help_text='Parent cluster (required)', limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.clusters'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tasks',
|
||||
name='keywords',
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['title'], name='igny8_conte_title_f13d63_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['content_type'], name='igny8_conte_content_df9458_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['content_structure'], name='igny8_conte_content_55cffb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['status'], name='igny8_conte_status_b7cba0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['external_id'], name='igny8_conte_externa_7ffbdf_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='content',
|
||||
index=models.Index(fields=['site', 'sector'], name='igny8_conte_site_id_dc7938_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tasks',
|
||||
index=models.Index(fields=['content_structure'], name='igny8_tasks_content_733577_idx'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ContentTaxonomyRelation',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tasks',
|
||||
name='keywords',
|
||||
field=models.ManyToManyField(blank=True, help_text='Keywords linked to this task', related_name='tasks', to='planner.keywords'),
|
||||
),
|
||||
]
|
||||
@@ -5,7 +5,7 @@ from .views import (
|
||||
ImagesViewSet,
|
||||
ContentViewSet,
|
||||
ContentTaxonomyViewSet,
|
||||
ContentAttributeViewSet,
|
||||
# ContentAttributeViewSet, # Disabled - serializer removed in Stage 1
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
@@ -13,7 +13,7 @@ router.register(r'tasks', TasksViewSet, basename='task')
|
||||
router.register(r'images', ImagesViewSet, basename='image')
|
||||
router.register(r'content', ContentViewSet, basename='content')
|
||||
router.register(r'taxonomies', ContentTaxonomyViewSet, basename='taxonomy')
|
||||
router.register(r'attributes', ContentAttributeViewSet, basename='attribute')
|
||||
# router.register(r'attributes', ContentAttributeViewSet, basename='attribute') # Disabled - serializer removed in Stage 1
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
@@ -16,9 +16,8 @@ from .serializers import (
|
||||
ImagesSerializer,
|
||||
ContentSerializer,
|
||||
ContentTaxonomySerializer,
|
||||
# ContentAttributeSerializer removed in Stage 1 - model no longer exists
|
||||
)
|
||||
from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute removed in Stage 1
|
||||
from igny8_core.business.content.models import ContentTaxonomy # ContentAttribute model exists but serializer removed in Stage 1
|
||||
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
|
||||
from igny8_core.business.content.services.validation_service import ContentValidationService
|
||||
from igny8_core.business.content.services.metadata_mapping_service import MetadataMappingService
|
||||
@@ -544,49 +543,19 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Also get content from images linked via task
|
||||
task_linked_images = Images.objects.filter(task__isnull=False, content__isnull=True)
|
||||
if account:
|
||||
task_linked_images = task_linked_images.filter(account=account)
|
||||
|
||||
# Apply site/sector filtering to task-linked images
|
||||
if site_id:
|
||||
try:
|
||||
task_linked_images = task_linked_images.filter(site_id=int(site_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if sector_id:
|
||||
try:
|
||||
task_linked_images = task_linked_images.filter(sector_id=int(sector_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Get content IDs from task-linked images
|
||||
task_content_ids = set()
|
||||
for image in task_linked_images:
|
||||
if image.task and hasattr(image.task, 'content_record'):
|
||||
try:
|
||||
content = image.task.content_record
|
||||
if content:
|
||||
task_content_ids.add(content.id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Combine both sets of content IDs
|
||||
content_ids = set(queryset.values_list('id', flat=True).distinct())
|
||||
content_ids.update(task_content_ids)
|
||||
# Task field removed in Stage 1 - images are now only linked to content directly
|
||||
# All images must be linked via content, not task
|
||||
|
||||
# Build grouped response
|
||||
grouped_data = []
|
||||
content_ids = set(queryset.values_list('id', flat=True).distinct())
|
||||
|
||||
for content_id in content_ids:
|
||||
try:
|
||||
content = Content.objects.get(id=content_id)
|
||||
|
||||
# Get images linked directly to content OR via task
|
||||
content_images = Images.objects.filter(
|
||||
Q(content=content) | Q(task=content.task)
|
||||
).order_by('position')
|
||||
# Get images linked directly to content
|
||||
content_images = Images.objects.filter(content=content).order_by('position')
|
||||
|
||||
# Get featured image
|
||||
featured_image = content_images.filter(image_type='featured').first()
|
||||
@@ -1585,82 +1554,7 @@ class ContentTaxonomyViewSet(SiteSectorModelViewSet):
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=['Writer - Attributes']),
|
||||
create=extend_schema(tags=['Writer - Attributes']),
|
||||
retrieve=extend_schema(tags=['Writer - Attributes']),
|
||||
update=extend_schema(tags=['Writer - Attributes']),
|
||||
partial_update=extend_schema(tags=['Writer - Attributes']),
|
||||
destroy=extend_schema(tags=['Writer - Attributes']),
|
||||
)
|
||||
class ContentAttributeViewSet(SiteSectorModelViewSet):
|
||||
"""
|
||||
ViewSet for managing content attributes (product specs, service modifiers, semantic facets)
|
||||
Unified API Standard v1.0 compliant
|
||||
"""
|
||||
queryset = ContentAttribute.objects.select_related('content', 'cluster', 'site', 'sector')
|
||||
serializer_class = ContentAttributeSerializer
|
||||
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
throttle_scope = 'writer'
|
||||
throttle_classes = [DebugScopedRateThrottle]
|
||||
|
||||
# DRF filtering configuration
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
|
||||
# Search configuration
|
||||
search_fields = ['name', 'value', 'external_attribute_name', 'content__title']
|
||||
|
||||
# Ordering configuration
|
||||
ordering_fields = ['name', 'attribute_type', 'created_at']
|
||||
ordering = ['attribute_type', 'name']
|
||||
|
||||
# Filter configuration
|
||||
filterset_fields = ['attribute_type', 'source', 'content', 'cluster', 'external_id']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Create attribute with site/sector context"""
|
||||
user = getattr(self.request, 'user', None)
|
||||
|
||||
try:
|
||||
query_params = getattr(self.request, 'query_params', None)
|
||||
if query_params is None:
|
||||
query_params = getattr(self.request, 'GET', {})
|
||||
except AttributeError:
|
||||
query_params = {}
|
||||
|
||||
site_id = serializer.validated_data.get('site_id') or query_params.get('site_id')
|
||||
sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id')
|
||||
|
||||
from igny8_core.auth.models import Site, Sector
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
if not site_id:
|
||||
raise ValidationError("site_id is required")
|
||||
|
||||
try:
|
||||
site = Site.objects.get(id=site_id)
|
||||
except Site.DoesNotExist:
|
||||
raise ValidationError(f"Site with id {site_id} does not exist")
|
||||
|
||||
if not sector_id:
|
||||
raise ValidationError("sector_id is required")
|
||||
|
||||
try:
|
||||
sector = Sector.objects.get(id=sector_id)
|
||||
if sector.site_id != site_id:
|
||||
raise ValidationError(f"Sector does not belong to the selected site")
|
||||
except Sector.DoesNotExist:
|
||||
raise ValidationError(f"Sector with id {sector_id} does not exist")
|
||||
|
||||
serializer.validated_data.pop('site_id', None)
|
||||
serializer.validated_data.pop('sector_id', None)
|
||||
|
||||
account = getattr(self.request, 'account', None)
|
||||
if not account and user and user.is_authenticated and user.account:
|
||||
account = user.account
|
||||
if not account:
|
||||
account = site.account
|
||||
|
||||
serializer.save(account=account, site=site, sector=sector)
|
||||
# ContentAttributeViewSet temporarily disabled - ContentAttributeSerializer was removed in Stage 1
|
||||
# TODO: Re-implement or remove completely based on Stage 1 architecture decisions
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user