978 lines
40 KiB
Python
978 lines
40 KiB
Python
from django.contrib import admin
|
|
from django.contrib import messages
|
|
from unfold.admin import ModelAdmin, TabularInline
|
|
from igny8_core.admin.base import SiteSectorAdminMixin, Igny8ModelAdmin
|
|
from .models import Tasks, Images, Content, ImagePrompts
|
|
from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute, ContentTaxonomyRelation, ContentClusterMap
|
|
from import_export.admin import ExportMixin, ImportExportMixin
|
|
from import_export import resources
|
|
|
|
|
|
class ContentTaxonomyInline(TabularInline):
|
|
"""Inline admin for managing content taxonomy relationships"""
|
|
model = ContentTaxonomyRelation
|
|
extra = 1
|
|
autocomplete_fields = ['taxonomy']
|
|
verbose_name = 'Taxonomy Term'
|
|
verbose_name_plural = 'Taxonomy Terms (Tags & Categories)'
|
|
|
|
|
|
class TaskResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Tasks"""
|
|
class Meta:
|
|
model = Tasks
|
|
fields = ('id', 'title', 'description', 'status', 'content_type', 'content_structure',
|
|
'site__name', 'sector__name', 'cluster__name', 'created_at', 'updated_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(Tasks)
|
|
class TasksAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = TaskResource
|
|
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'status', 'cluster', 'created_at']
|
|
list_editable = ['status'] # Enable inline editing for status
|
|
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'cluster', 'created_at']
|
|
search_fields = ['title', 'description']
|
|
ordering = ['-created_at']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
autocomplete_fields = ['cluster', 'site', 'sector']
|
|
actions = [
|
|
'bulk_set_status_draft',
|
|
'bulk_set_status_in_progress',
|
|
'bulk_set_status_completed',
|
|
'bulk_assign_cluster',
|
|
'bulk_soft_delete',
|
|
'bulk_update_content_type',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('title', 'description', 'status', 'site', 'sector')
|
|
}),
|
|
('Content Classification', {
|
|
'fields': ('content_type', 'content_structure', 'taxonomy_term')
|
|
}),
|
|
('Planning', {
|
|
'fields': ('cluster', 'keywords')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def bulk_set_status_draft(self, request, queryset):
|
|
"""Set selected tasks to draft status"""
|
|
updated = queryset.update(status='draft')
|
|
self.message_user(request, f'{updated} task(s) set to draft.', messages.SUCCESS)
|
|
bulk_set_status_draft.short_description = 'Set status to Draft'
|
|
|
|
def bulk_set_status_in_progress(self, request, queryset):
|
|
"""Set selected tasks to in-progress status"""
|
|
updated = queryset.update(status='in_progress')
|
|
self.message_user(request, f'{updated} task(s) set to in progress.', messages.SUCCESS)
|
|
bulk_set_status_in_progress.short_description = 'Set status to In Progress'
|
|
|
|
def bulk_set_status_completed(self, request, queryset):
|
|
"""Set selected tasks to completed status"""
|
|
updated = queryset.update(status='completed')
|
|
self.message_user(request, f'{updated} task(s) set to completed.', messages.SUCCESS)
|
|
bulk_set_status_completed.short_description = 'Set status to Completed'
|
|
|
|
def bulk_assign_cluster(self, request, queryset):
|
|
"""Assign selected tasks to a cluster - requires form input"""
|
|
from django import forms
|
|
from igny8_core.modules.planner.models import Clusters
|
|
|
|
# If this is the POST request with cluster selection
|
|
if 'apply' in request.POST:
|
|
cluster_id = request.POST.get('cluster')
|
|
if cluster_id:
|
|
cluster = Clusters.objects.get(pk=cluster_id)
|
|
updated = queryset.update(cluster=cluster)
|
|
self.message_user(request, f'{updated} task(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
|
|
return
|
|
|
|
# Get first task's site/sector for filtering clusters
|
|
first_task = queryset.first()
|
|
if first_task:
|
|
clusters = Clusters.objects.filter(site=first_task.site, sector=first_task.sector)
|
|
else:
|
|
clusters = Clusters.objects.all()
|
|
|
|
# Create form for cluster selection
|
|
class ClusterForm(forms.Form):
|
|
cluster = forms.ModelChoiceField(
|
|
queryset=clusters,
|
|
label="Select Cluster",
|
|
help_text=f"Assign {queryset.count()} selected task(s) to:"
|
|
)
|
|
|
|
if clusters.exists():
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Assign Tasks to Cluster',
|
|
'queryset': queryset,
|
|
'form': ClusterForm(),
|
|
'action': 'bulk_assign_cluster',
|
|
})
|
|
else:
|
|
self.message_user(request, 'No clusters available for the selected tasks.', messages.WARNING)
|
|
bulk_assign_cluster.short_description = 'Assign to Cluster'
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected tasks"""
|
|
count = 0
|
|
for task in queryset:
|
|
task.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} task(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected tasks'
|
|
|
|
def bulk_update_content_type(self, request, queryset):
|
|
"""Update content type for selected tasks"""
|
|
from django import forms
|
|
|
|
if 'apply' in request.POST:
|
|
content_type = request.POST.get('content_type')
|
|
if content_type:
|
|
updated = queryset.update(content_type=content_type)
|
|
self.message_user(request, f'{updated} task(s) updated to content type: {content_type}', messages.SUCCESS)
|
|
return
|
|
|
|
# Get content type choices from model
|
|
CONTENT_TYPE_CHOICES = [
|
|
('blog_post', 'Blog Post'),
|
|
('article', 'Article'),
|
|
('product', 'Product'),
|
|
('service', 'Service'),
|
|
('page', 'Page'),
|
|
('landing_page', 'Landing Page'),
|
|
]
|
|
|
|
class ContentTypeForm(forms.Form):
|
|
content_type = forms.ChoiceField(
|
|
choices=CONTENT_TYPE_CHOICES,
|
|
label="Select Content Type",
|
|
help_text=f"Update content type for {queryset.count()} selected task(s)"
|
|
)
|
|
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Update Content Type',
|
|
'queryset': queryset,
|
|
'form': ContentTypeForm(),
|
|
'action': 'bulk_update_content_type',
|
|
})
|
|
bulk_update_content_type.short_description = 'Update content type'
|
|
|
|
def get_site_display(self, obj):
|
|
"""Safely get site name"""
|
|
try:
|
|
return obj.site.name if obj.site else '-'
|
|
except:
|
|
return '-'
|
|
get_site_display.short_description = 'Site'
|
|
|
|
def get_sector_display(self, obj):
|
|
"""Safely get sector name"""
|
|
try:
|
|
return obj.sector.name if obj.sector else '-'
|
|
except:
|
|
return '-'
|
|
|
|
def get_cluster_display(self, obj):
|
|
"""Safely get cluster name"""
|
|
try:
|
|
return obj.cluster.name if obj.cluster else '-'
|
|
except:
|
|
return '-'
|
|
get_cluster_display.short_description = 'Cluster'
|
|
|
|
|
|
class ImagesResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Images"""
|
|
class Meta:
|
|
model = Images
|
|
fields = ('id', 'content__title', 'site__name', 'sector__name', 'image_type', 'status', 'position', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(Images)
|
|
class ImagesAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ImagesResource
|
|
list_display = ['get_content_title', 'site', 'sector', 'image_type', 'status', 'position', 'created_at']
|
|
list_filter = ['image_type', 'status', 'site', 'sector']
|
|
search_fields = ['content__title']
|
|
ordering = ['-id'] # Sort by ID descending (newest first)
|
|
actions = [
|
|
'bulk_set_status_published',
|
|
'bulk_set_status_draft',
|
|
'bulk_set_type_featured',
|
|
'bulk_set_type_inline',
|
|
'bulk_set_type_thumbnail',
|
|
'bulk_soft_delete',
|
|
]
|
|
|
|
def get_content_title(self, obj):
|
|
"""Get content title, fallback to task title if no content"""
|
|
if obj.content:
|
|
return obj.content.title or obj.content.meta_title or f"Content #{obj.content.id}"
|
|
elif obj.task:
|
|
return obj.task.title or f"Task #{obj.task.id}"
|
|
return '-'
|
|
get_content_title.short_description = 'Content Title'
|
|
|
|
def get_site_display(self, obj):
|
|
"""Safely get site name"""
|
|
try:
|
|
return obj.site.name if obj.site else '-'
|
|
except:
|
|
return '-'
|
|
get_site_display.short_description = 'Site'
|
|
|
|
def get_sector_display(self, obj):
|
|
"""Safely get sector name"""
|
|
try:
|
|
return obj.sector.name if obj.sector else '-'
|
|
except:
|
|
return '-'
|
|
|
|
def bulk_set_status_published(self, request, queryset):
|
|
"""Set selected images to published status"""
|
|
updated = queryset.update(status='published')
|
|
self.message_user(request, f'{updated} image(s) set to published.', messages.SUCCESS)
|
|
bulk_set_status_published.short_description = 'Set status to Published'
|
|
|
|
def bulk_set_status_draft(self, request, queryset):
|
|
"""Set selected images to draft status"""
|
|
updated = queryset.update(status='draft')
|
|
self.message_user(request, f'{updated} image(s) set to draft.', messages.SUCCESS)
|
|
bulk_set_status_draft.short_description = 'Set status to Draft'
|
|
|
|
def bulk_set_type_featured(self, request, queryset):
|
|
"""Set selected images to featured type"""
|
|
updated = queryset.update(image_type='featured')
|
|
self.message_user(request, f'{updated} image(s) set to featured.', messages.SUCCESS)
|
|
bulk_set_type_featured.short_description = 'Set type to Featured'
|
|
|
|
def bulk_set_type_inline(self, request, queryset):
|
|
"""Set selected images to inline type"""
|
|
updated = queryset.update(image_type='inline')
|
|
self.message_user(request, f'{updated} image(s) set to inline.', messages.SUCCESS)
|
|
bulk_set_type_inline.short_description = 'Set type to Inline'
|
|
|
|
def bulk_set_type_thumbnail(self, request, queryset):
|
|
"""Set selected images to thumbnail type"""
|
|
updated = queryset.update(image_type='thumbnail')
|
|
self.message_user(request, f'{updated} image(s) set to thumbnail.', messages.SUCCESS)
|
|
bulk_set_type_thumbnail.short_description = 'Set type to Thumbnail'
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected images"""
|
|
count = 0
|
|
for image in queryset:
|
|
image.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} image(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected images'
|
|
|
|
|
|
# ============================================================================
|
|
# Image Prompts Admin (Using Proxy Model from models.py)
|
|
# ============================================================================
|
|
|
|
class ImagePromptsResource(resources.ModelResource):
|
|
"""Resource class for exporting Image Prompts"""
|
|
class Meta:
|
|
model = ImagePrompts
|
|
fields = ('id', 'content__title', 'site__name', 'sector__name', 'image_type', 'prompt', 'caption', 'status', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(ImagePrompts)
|
|
class ImagePromptsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
"""
|
|
Specialized admin for viewing and managing image prompts.
|
|
This provides a focused view of the prompt field from Images model.
|
|
"""
|
|
resource_class = ImagePromptsResource
|
|
|
|
list_display = ['get_content_title', 'site', 'sector', 'image_type', 'get_prompt_preview', 'status', 'created_at']
|
|
list_filter = ['image_type', 'status', 'site', 'sector', 'created_at']
|
|
search_fields = ['content__title', 'prompt', 'caption']
|
|
ordering = ['-created_at']
|
|
readonly_fields = ['get_content_title', 'site', 'sector', 'image_type', 'prompt', 'caption',
|
|
'status', 'position', 'image_url', 'image_path', 'created_at', 'updated_at']
|
|
|
|
actions = [
|
|
'bulk_export_prompts',
|
|
'bulk_copy_prompts_to_clipboard',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Content Information', {
|
|
'fields': ('get_content_title', 'site', 'sector', 'image_type', 'position')
|
|
}),
|
|
('Prompt Details', {
|
|
'fields': ('prompt', 'caption'),
|
|
'description': 'AI-generated prompts used for image creation'
|
|
}),
|
|
('Image Information', {
|
|
'fields': ('status', 'image_url', 'image_path'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def get_prompt_preview(self, obj):
|
|
"""Display a truncated preview of the prompt"""
|
|
if obj.prompt:
|
|
return obj.prompt[:100] + '...' if len(obj.prompt) > 100 else obj.prompt
|
|
return '-'
|
|
get_prompt_preview.short_description = 'Prompt Preview'
|
|
|
|
def get_content_title(self, obj):
|
|
"""Get content title, fallback to task title if no content"""
|
|
if obj.content:
|
|
return obj.content.title or obj.content.meta_title or f"Content #{obj.content.id}"
|
|
elif obj.task:
|
|
return obj.task.title or f"Task #{obj.task.id}"
|
|
return '-'
|
|
get_content_title.short_description = 'Content'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter to only show images that have prompts"""
|
|
qs = super().get_queryset(request)
|
|
return qs.filter(prompt__isnull=False).exclude(prompt='')
|
|
|
|
def has_add_permission(self, request):
|
|
"""Image prompts are created through content generation workflow"""
|
|
return False
|
|
|
|
def has_change_permission(self, request, obj=None):
|
|
"""Image prompts are read-only"""
|
|
return False
|
|
|
|
def has_delete_permission(self, request, obj=None):
|
|
"""Prevent deletion from this view (use Images admin instead)"""
|
|
return False
|
|
|
|
def bulk_export_prompts(self, request, queryset):
|
|
"""Export selected image prompts to CSV"""
|
|
import csv
|
|
from django.http import HttpResponse
|
|
from datetime import datetime
|
|
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = f'attachment; filename="image_prompts_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow(['Content Title', 'Site', 'Image Type', 'Prompt', 'Caption', 'Status', 'Created'])
|
|
|
|
for obj in queryset:
|
|
content_title = self.get_content_title(obj)
|
|
site_name = obj.site.name if obj.site else '-'
|
|
writer.writerow([
|
|
content_title,
|
|
site_name,
|
|
obj.image_type,
|
|
obj.prompt or '',
|
|
obj.caption or '',
|
|
obj.status,
|
|
obj.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
|
])
|
|
|
|
self.message_user(request, f'{queryset.count()} image prompt(s) exported to CSV.', messages.SUCCESS)
|
|
return response
|
|
bulk_export_prompts.short_description = 'Export prompts to CSV'
|
|
|
|
def bulk_copy_prompts_to_clipboard(self, request, queryset):
|
|
"""Generate a text summary of prompts for copying"""
|
|
prompts_text = []
|
|
for obj in queryset:
|
|
content_title = self.get_content_title(obj)
|
|
prompts_text.append(f"--- {content_title} ({obj.image_type}) ---")
|
|
prompts_text.append(f"Prompt: {obj.prompt or 'N/A'}")
|
|
if obj.caption:
|
|
prompts_text.append(f"Caption: {obj.caption}")
|
|
prompts_text.append("")
|
|
|
|
# Store in session for display
|
|
request.session['prompts_export'] = '\n'.join(prompts_text)
|
|
self.message_user(
|
|
request,
|
|
f'Generated text for {queryset.count()} prompt(s). Copy from the message below.',
|
|
messages.INFO
|
|
)
|
|
bulk_copy_prompts_to_clipboard.short_description = 'Copy prompts as text'
|
|
|
|
|
|
class ContentResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Content"""
|
|
class Meta:
|
|
model = Content
|
|
fields = ('id', 'title', 'content_type', 'content_structure', 'status', 'source',
|
|
'site__name', 'sector__name', 'cluster__name', 'word_count',
|
|
'meta_title', 'meta_description', 'primary_keyword', 'secondary_keywords',
|
|
'content_html', 'external_url', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(Content)
|
|
class ContentAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ContentResource
|
|
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'word_count', 'get_taxonomy_count', 'created_at']
|
|
list_filter = ['status', 'content_type', 'content_structure', 'source', 'site', 'sector', 'cluster', 'word_count', 'created_at']
|
|
search_fields = ['title', 'content_html', 'external_url', 'meta_title', 'primary_keyword']
|
|
ordering = ['-created_at']
|
|
readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display']
|
|
autocomplete_fields = ['cluster', 'site', 'sector']
|
|
inlines = [ContentTaxonomyInline]
|
|
actions = [
|
|
'bulk_set_status_published',
|
|
'bulk_set_status_draft',
|
|
'bulk_add_taxonomy',
|
|
'bulk_soft_delete',
|
|
'bulk_publish_to_wordpress',
|
|
'bulk_unpublish_from_wordpress',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('title', 'site', 'sector', 'cluster', 'status')
|
|
}),
|
|
('Content Classification', {
|
|
'fields': ('content_type', 'content_structure', 'source')
|
|
}),
|
|
('Taxonomies (Read-only - manage below)', {
|
|
'fields': ('get_tags_display', 'get_categories_display'),
|
|
'classes': ('collapse',),
|
|
'description': 'View tags and categories. To add/remove, use the Taxonomy Terms section below.'
|
|
}),
|
|
('Content', {
|
|
'fields': ('content_html', 'word_count')
|
|
}),
|
|
('SEO', {
|
|
'fields': ('meta_title', 'meta_description', 'primary_keyword', 'secondary_keywords'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('WordPress Sync', {
|
|
'fields': ('external_id', 'external_url'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def get_taxonomy_count(self, obj):
|
|
"""Display count of associated taxonomy terms"""
|
|
return obj.taxonomy_terms.count()
|
|
get_taxonomy_count.short_description = 'Taxonomy Count'
|
|
|
|
def get_tags_display(self, obj):
|
|
"""Display tags"""
|
|
tags = obj.taxonomy_terms.filter(taxonomy_type='tag')
|
|
if tags.exists():
|
|
return ', '.join([tag.name for tag in tags])
|
|
return 'No tags'
|
|
get_tags_display.short_description = 'Tags'
|
|
|
|
def get_categories_display(self, obj):
|
|
"""Display categories"""
|
|
categories = obj.taxonomy_terms.filter(taxonomy_type='category')
|
|
if categories.exists():
|
|
return ', '.join([cat.name for cat in categories])
|
|
return 'No categories'
|
|
get_categories_display.short_description = 'Categories'
|
|
|
|
def bulk_set_status_published(self, request, queryset):
|
|
"""Set selected content to published status"""
|
|
updated = queryset.update(status='published')
|
|
self.message_user(request, f'{updated} content item(s) set to published.', messages.SUCCESS)
|
|
bulk_set_status_published.short_description = 'Set status to Published'
|
|
|
|
def bulk_set_status_draft(self, request, queryset):
|
|
"""Set selected content to draft status"""
|
|
updated = queryset.update(status='draft')
|
|
self.message_user(request, f'{updated} content item(s) set to draft.', messages.SUCCESS)
|
|
bulk_set_status_draft.short_description = 'Set status to Draft'
|
|
|
|
def bulk_add_taxonomy(self, request, queryset):
|
|
"""Add taxonomy terms to selected content"""
|
|
from django import forms
|
|
from igny8_core.business.content.models import ContentTaxonomy, ContentTaxonomyRelation
|
|
|
|
# If this is the POST request with taxonomy selection
|
|
if 'apply' in request.POST:
|
|
taxonomy_ids = request.POST.getlist('taxonomies')
|
|
if taxonomy_ids:
|
|
count = 0
|
|
for content in queryset:
|
|
for tax_id in taxonomy_ids:
|
|
taxonomy = ContentTaxonomy.objects.get(pk=tax_id)
|
|
ContentTaxonomyRelation.objects.get_or_create(
|
|
content=content,
|
|
taxonomy=taxonomy
|
|
)
|
|
count += 1
|
|
self.message_user(request, f'Added {count} taxonomy relation(s) to {queryset.count()} content item(s).', messages.SUCCESS)
|
|
return
|
|
|
|
# Get first content's site/sector for filtering taxonomies
|
|
first_content = queryset.first()
|
|
if first_content:
|
|
taxonomies = ContentTaxonomy.objects.filter(site=first_content.site, sector=first_content.sector)
|
|
else:
|
|
taxonomies = ContentTaxonomy.objects.all()
|
|
|
|
# Create form for taxonomy selection
|
|
class TaxonomyForm(forms.Form):
|
|
taxonomies = forms.ModelMultipleChoiceField(
|
|
queryset=taxonomies,
|
|
label="Select Taxonomies",
|
|
help_text=f"Add taxonomy terms to {queryset.count()} selected content item(s)",
|
|
widget=forms.CheckboxSelectMultiple
|
|
)
|
|
|
|
if taxonomies.exists():
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Add Taxonomies to Content',
|
|
'queryset': queryset,
|
|
'form': TaxonomyForm(),
|
|
'action': 'bulk_add_taxonomy',
|
|
})
|
|
else:
|
|
self.message_user(request, 'No taxonomies available for the selected content.', messages.WARNING)
|
|
bulk_add_taxonomy.short_description = 'Add Taxonomy Terms'
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected content"""
|
|
count = 0
|
|
for content in queryset:
|
|
content.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} content item(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected content'
|
|
|
|
def bulk_publish_to_wordpress(self, request, queryset):
|
|
"""Publish selected content to WordPress"""
|
|
from igny8_core.business.publishing.models import PublishingRecord
|
|
|
|
count = 0
|
|
for content in queryset:
|
|
if content.site:
|
|
# Create publishing record for WordPress
|
|
PublishingRecord.objects.get_or_create(
|
|
content=content,
|
|
site=content.site,
|
|
sector=content.sector,
|
|
account=content.account,
|
|
destination='wordpress',
|
|
defaults={
|
|
'status': 'pending',
|
|
'metadata': {}
|
|
}
|
|
)
|
|
count += 1
|
|
|
|
self.message_user(request, f'{count} content item(s) queued for WordPress publishing.', messages.SUCCESS)
|
|
bulk_publish_to_wordpress.short_description = 'Publish to WordPress'
|
|
|
|
def bulk_unpublish_from_wordpress(self, request, queryset):
|
|
"""Unpublish/remove selected content from WordPress"""
|
|
from igny8_core.business.publishing.models import PublishingRecord
|
|
|
|
count = 0
|
|
for content in queryset:
|
|
# Update existing publishing records to mark for removal
|
|
records = PublishingRecord.objects.filter(
|
|
content=content,
|
|
destination='wordpress',
|
|
status__in=['published', 'pending', 'publishing']
|
|
)
|
|
records.update(status='failed', error_message='Unpublish requested from admin')
|
|
count += records.count()
|
|
|
|
self.message_user(request, f'{count} publishing record(s) marked for unpublish.', messages.SUCCESS)
|
|
bulk_unpublish_from_wordpress.short_description = 'Unpublish from WordPress'
|
|
|
|
def get_site_display(self, obj):
|
|
"""Safely get site name"""
|
|
try:
|
|
return obj.site.name if obj.site else '-'
|
|
except:
|
|
return '-'
|
|
get_site_display.short_description = 'Site'
|
|
|
|
def get_sector_display(self, obj):
|
|
"""Safely get sector name"""
|
|
try:
|
|
return obj.sector.name if obj.sector else '-'
|
|
except:
|
|
return '-'
|
|
|
|
|
|
class ContentTaxonomyResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Content Taxonomies"""
|
|
class Meta:
|
|
model = ContentTaxonomy
|
|
fields = ('id', 'name', 'slug', 'taxonomy_type', 'description', 'site__name', 'sector__name',
|
|
'count', 'external_id', 'external_taxonomy', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(ContentTaxonomy)
|
|
class ContentTaxonomyAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ContentTaxonomyResource
|
|
list_display = ['name', 'taxonomy_type', 'slug', 'count', 'external_id', 'external_taxonomy', 'site', 'sector']
|
|
list_filter = ['taxonomy_type', 'site', 'sector']
|
|
search_fields = ['name', 'slug', 'external_taxonomy']
|
|
ordering = ['taxonomy_type', 'name']
|
|
readonly_fields = ['count', 'created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_soft_delete',
|
|
'bulk_merge_taxonomies',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('name', 'slug', 'taxonomy_type', 'description', 'site', 'sector')
|
|
}),
|
|
('Usage', {
|
|
'fields': ('count',),
|
|
'description': 'Number of content items using this term'
|
|
}),
|
|
('WordPress Sync', {
|
|
'fields': ('external_id', 'external_taxonomy', 'metadata'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('site', 'sector')
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Delete selected taxonomies"""
|
|
count = queryset.count()
|
|
queryset.delete()
|
|
self.message_user(request, f'{count} taxonomy/taxonomies deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Delete selected taxonomies'
|
|
|
|
def bulk_merge_taxonomies(self, request, queryset):
|
|
"""Merge selected taxonomies into one"""
|
|
from django import forms
|
|
from igny8_core.business.content.models import ContentTaxonomyRelation
|
|
|
|
if 'apply' in request.POST:
|
|
target_id = request.POST.get('target_taxonomy')
|
|
if target_id:
|
|
target = ContentTaxonomy.objects.get(pk=target_id)
|
|
merged_count = 0
|
|
|
|
for taxonomy in queryset.exclude(pk=target.pk):
|
|
# Move all relations to target
|
|
ContentTaxonomyRelation.objects.filter(taxonomy=taxonomy).update(taxonomy=target)
|
|
taxonomy.delete()
|
|
merged_count += 1
|
|
|
|
# Update target count
|
|
target.count = ContentTaxonomyRelation.objects.filter(taxonomy=target).count()
|
|
target.save()
|
|
|
|
self.message_user(request, f'{merged_count} taxonomies merged into: {target.name}', messages.SUCCESS)
|
|
return
|
|
|
|
class MergeForm(forms.Form):
|
|
target_taxonomy = forms.ModelChoiceField(
|
|
queryset=queryset,
|
|
label="Merge into",
|
|
help_text="Select the taxonomy to keep (others will be merged into this one)"
|
|
)
|
|
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Merge Taxonomies',
|
|
'queryset': queryset,
|
|
'form': MergeForm(),
|
|
'action': 'bulk_merge_taxonomies',
|
|
})
|
|
bulk_merge_taxonomies.short_description = 'Merge selected taxonomies'
|
|
|
|
|
|
class ContentAttributeResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Content Attributes"""
|
|
class Meta:
|
|
model = ContentAttribute
|
|
fields = ('id', 'name', 'value', 'attribute_type', 'content__title', 'cluster__name',
|
|
'external_id', 'source', 'site__name', 'sector__name', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(ContentAttribute)
|
|
class ContentAttributeAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ContentAttributeResource
|
|
list_display = ['name', 'value', 'attribute_type', 'content', 'cluster', 'external_id', 'source', 'site', 'sector']
|
|
list_filter = ['attribute_type', 'source', 'site', 'sector']
|
|
search_fields = ['name', 'value', 'external_attribute_name', 'content__title']
|
|
ordering = ['attribute_type', 'name']
|
|
actions = [
|
|
'bulk_soft_delete',
|
|
'bulk_update_attribute_type',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('attribute_type', 'name', 'value', 'site', 'sector')
|
|
}),
|
|
('Relationships', {
|
|
'fields': ('content', 'cluster'),
|
|
'description': 'Link to content (products/services) or cluster (semantic attributes).'
|
|
}),
|
|
('WordPress/WooCommerce Sync', {
|
|
'fields': ('external_id', 'external_attribute_name', 'source', 'metadata')
|
|
}),
|
|
)
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related('content', 'cluster', 'site', 'sector')
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Delete selected attributes"""
|
|
count = queryset.count()
|
|
queryset.delete()
|
|
self.message_user(request, f'{count} attribute(s) deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Delete selected attributes'
|
|
|
|
def bulk_update_attribute_type(self, request, queryset):
|
|
"""Update attribute type for selected attributes"""
|
|
from django import forms
|
|
|
|
if 'apply' in request.POST:
|
|
attr_type = request.POST.get('attribute_type')
|
|
if attr_type:
|
|
updated = queryset.update(attribute_type=attr_type)
|
|
self.message_user(request, f'{updated} attribute(s) updated to type: {attr_type}', messages.SUCCESS)
|
|
return
|
|
|
|
ATTR_TYPE_CHOICES = [
|
|
('product', 'Product Attribute'),
|
|
('service', 'Service Attribute'),
|
|
('semantic', 'Semantic Attribute'),
|
|
('technical', 'Technical Attribute'),
|
|
]
|
|
|
|
class AttributeTypeForm(forms.Form):
|
|
attribute_type = forms.ChoiceField(
|
|
choices=ATTR_TYPE_CHOICES,
|
|
label="Select Attribute Type",
|
|
help_text=f"Update attribute type for {queryset.count()} selected attribute(s)"
|
|
)
|
|
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Update Attribute Type',
|
|
'queryset': queryset,
|
|
'form': AttributeTypeForm(),
|
|
'action': 'bulk_update_attribute_type',
|
|
})
|
|
bulk_update_attribute_type.short_description = 'Update attribute type'
|
|
|
|
|
|
class ContentTaxonomyRelationResource(resources.ModelResource):
|
|
"""Resource class for exporting Content Taxonomy Relations"""
|
|
class Meta:
|
|
model = ContentTaxonomyRelation
|
|
fields = ('id', 'content__title', 'taxonomy__name', 'taxonomy__taxonomy_type', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(ContentTaxonomyRelation)
|
|
class ContentTaxonomyRelationAdmin(ExportMixin, Igny8ModelAdmin):
|
|
resource_class = ContentTaxonomyRelationResource
|
|
list_display = ['content', 'taxonomy', 'created_at']
|
|
search_fields = ['content__title', 'taxonomy__name']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_delete_relations',
|
|
'bulk_reassign_taxonomy',
|
|
]
|
|
|
|
def bulk_delete_relations(self, request, queryset):
|
|
count = queryset.count()
|
|
queryset.delete()
|
|
self.message_user(request, f'{count} content taxonomy relation(s) deleted.', messages.SUCCESS)
|
|
bulk_delete_relations.short_description = 'Delete selected relations'
|
|
|
|
def bulk_reassign_taxonomy(self, request, queryset):
|
|
"""Admin action to bulk reassign taxonomy - requires intermediate page"""
|
|
if 'apply' in request.POST:
|
|
from django import forms
|
|
from .models import ContentTaxonomy
|
|
|
|
class TaxonomyForm(forms.Form):
|
|
taxonomy = forms.ModelChoiceField(
|
|
queryset=ContentTaxonomy.objects.filter(is_active=True),
|
|
required=True,
|
|
label='New Taxonomy'
|
|
)
|
|
|
|
form = TaxonomyForm(request.POST)
|
|
if form.is_valid():
|
|
new_taxonomy = form.cleaned_data['taxonomy']
|
|
count = queryset.update(taxonomy=new_taxonomy)
|
|
self.message_user(request, f'{count} relation(s) reassigned to {new_taxonomy.name}.', messages.SUCCESS)
|
|
return
|
|
|
|
from django import forms
|
|
from .models import ContentTaxonomy
|
|
|
|
class TaxonomyForm(forms.Form):
|
|
taxonomy = forms.ModelChoiceField(
|
|
queryset=ContentTaxonomy.objects.filter(is_active=True),
|
|
required=True,
|
|
label='New Taxonomy',
|
|
help_text='Select the taxonomy to reassign these relations to'
|
|
)
|
|
|
|
context = {
|
|
'title': 'Bulk Reassign Taxonomy',
|
|
'queryset': queryset,
|
|
'form': TaxonomyForm(),
|
|
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
|
|
}
|
|
return render(request, 'admin/bulk_action_form.html', context)
|
|
bulk_reassign_taxonomy.short_description = 'Reassign taxonomy'
|
|
|
|
|
|
class ContentClusterMapResource(resources.ModelResource):
|
|
"""Resource class for exporting Content Cluster Maps"""
|
|
class Meta:
|
|
model = ContentClusterMap
|
|
fields = ('id', 'content__title', 'task__title', 'cluster__name',
|
|
'role', 'source', 'site__name', 'sector__name', 'created_at')
|
|
export_order = fields
|
|
|
|
|
|
@admin.register(ContentClusterMap)
|
|
class ContentClusterMapAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ContentClusterMapResource
|
|
list_display = ['content', 'task', 'cluster', 'role', 'source', 'site', 'sector', 'created_at']
|
|
list_filter = ['role', 'source', 'site', 'sector']
|
|
search_fields = ['content__title', 'task__title', 'cluster__name']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_delete_maps',
|
|
'bulk_update_role',
|
|
'bulk_reassign_cluster',
|
|
]
|
|
|
|
def bulk_delete_maps(self, request, queryset):
|
|
count = queryset.count()
|
|
queryset.delete()
|
|
self.message_user(request, f'{count} content cluster map(s) deleted.', messages.SUCCESS)
|
|
bulk_delete_maps.short_description = 'Delete selected maps'
|
|
|
|
def bulk_update_role(self, request, queryset):
|
|
"""Admin action to bulk update role"""
|
|
if 'apply' in request.POST:
|
|
from django import forms
|
|
|
|
class RoleForm(forms.Form):
|
|
ROLE_CHOICES = [
|
|
('pillar', 'Pillar'),
|
|
('supporting', 'Supporting'),
|
|
('related', 'Related'),
|
|
]
|
|
role = forms.ChoiceField(choices=ROLE_CHOICES, required=True)
|
|
|
|
form = RoleForm(request.POST)
|
|
if form.is_valid():
|
|
new_role = form.cleaned_data['role']
|
|
count = queryset.update(role=new_role)
|
|
self.message_user(request, f'{count} map(s) updated to role: {new_role}.', messages.SUCCESS)
|
|
return
|
|
|
|
from django import forms
|
|
|
|
class RoleForm(forms.Form):
|
|
ROLE_CHOICES = [
|
|
('pillar', 'Pillar'),
|
|
('supporting', 'Supporting'),
|
|
('related', 'Related'),
|
|
]
|
|
role = forms.ChoiceField(
|
|
choices=ROLE_CHOICES,
|
|
required=True,
|
|
help_text='Select the new role for these content cluster maps'
|
|
)
|
|
|
|
context = {
|
|
'title': 'Bulk Update Role',
|
|
'queryset': queryset,
|
|
'form': RoleForm(),
|
|
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
|
|
}
|
|
return render(request, 'admin/bulk_action_form.html', context)
|
|
bulk_update_role.short_description = 'Update role'
|
|
|
|
def bulk_reassign_cluster(self, request, queryset):
|
|
"""Admin action to bulk reassign cluster"""
|
|
if 'apply' in request.POST:
|
|
from django import forms
|
|
from igny8_core.business.planner.models import Cluster
|
|
|
|
class ClusterForm(forms.Form):
|
|
cluster = forms.ModelChoiceField(
|
|
queryset=Cluster.objects.filter(is_active=True),
|
|
required=True,
|
|
label='New Cluster'
|
|
)
|
|
|
|
form = ClusterForm(request.POST)
|
|
if form.is_valid():
|
|
new_cluster = form.cleaned_data['cluster']
|
|
count = queryset.update(cluster=new_cluster)
|
|
self.message_user(request, f'{count} map(s) reassigned to cluster: {new_cluster.name}.', messages.SUCCESS)
|
|
return
|
|
|
|
from django import forms
|
|
from igny8_core.business.planner.models import Cluster
|
|
|
|
class ClusterForm(forms.Form):
|
|
cluster = forms.ModelChoiceField(
|
|
queryset=Cluster.objects.filter(is_active=True),
|
|
required=True,
|
|
label='New Cluster',
|
|
help_text='Select the cluster to reassign these maps to'
|
|
)
|
|
|
|
context = {
|
|
'title': 'Bulk Reassign Cluster',
|
|
'queryset': queryset,
|
|
'form': ClusterForm(),
|
|
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
|
|
}
|
|
return render(request, 'admin/bulk_action_form.html', context)
|
|
bulk_reassign_cluster.short_description = 'Reassign cluster'
|