397 lines
16 KiB
Python
397 lines
16 KiB
Python
from django.contrib import admin
|
|
from django.contrib import messages
|
|
from unfold.admin import ModelAdmin
|
|
from igny8_core.admin.base import SiteSectorAdminMixin, Igny8ModelAdmin
|
|
from .models import Keywords, Clusters, ContentIdeas
|
|
from import_export.admin import ExportMixin, ImportExportMixin
|
|
from import_export import resources
|
|
|
|
|
|
class KeywordsResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Keywords"""
|
|
class Meta:
|
|
model = Keywords
|
|
fields = ('id', 'keyword', 'seed_keyword__keyword', 'site__name', 'sector__name',
|
|
'cluster__name', 'volume', 'difficulty', 'country', 'status', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
class ClustersResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Clusters"""
|
|
class Meta:
|
|
model = Clusters
|
|
fields = ('id', 'name', 'site__name', 'sector__name', 'keywords_count', 'volume',
|
|
'status', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(Clusters)
|
|
class ClustersAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ClustersResource
|
|
list_display = ['name', 'site', 'sector', 'keywords_count', 'volume', 'status', 'created_at']
|
|
list_select_related = ['site', 'sector', 'account']
|
|
list_filter = ['status', 'site', 'sector', 'volume', 'created_at']
|
|
search_fields = ['name']
|
|
ordering = ['name']
|
|
autocomplete_fields = ['site', 'sector']
|
|
actions = [
|
|
'bulk_set_status_active',
|
|
'bulk_set_status_inactive',
|
|
'bulk_soft_delete',
|
|
]
|
|
|
|
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_active(self, request, queryset):
|
|
"""Set selected clusters to active status"""
|
|
updated = queryset.update(status='active')
|
|
self.message_user(request, f'{updated} cluster(s) set to active.', messages.SUCCESS)
|
|
bulk_set_status_active.short_description = 'Set status to Active'
|
|
|
|
def bulk_set_status_inactive(self, request, queryset):
|
|
"""Set selected clusters to inactive status"""
|
|
updated = queryset.update(status='inactive')
|
|
self.message_user(request, f'{updated} cluster(s) set to inactive.', messages.SUCCESS)
|
|
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected clusters"""
|
|
count = 0
|
|
for cluster in queryset:
|
|
cluster.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} cluster(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected clusters'
|
|
|
|
|
|
@admin.register(Keywords)
|
|
class KeywordsAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = KeywordsResource
|
|
# Use actual DB fields and custom methods with @admin.display for computed values
|
|
list_display = ['get_keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'get_volume', 'get_difficulty', 'get_country', 'status', 'created_at']
|
|
list_editable = ['status'] # Enable inline editing for status
|
|
list_select_related = ['site', 'sector', 'cluster', 'seed_keyword', 'seed_keyword__industry', 'seed_keyword__sector', 'account']
|
|
list_filter = ['status', 'site', 'sector', 'cluster', 'created_at']
|
|
search_fields = ['seed_keyword__keyword']
|
|
ordering = ['-created_at']
|
|
autocomplete_fields = ['cluster', 'site', 'sector', 'seed_keyword']
|
|
actions = [
|
|
'bulk_assign_cluster',
|
|
'bulk_set_status_active',
|
|
'bulk_set_status_inactive',
|
|
'bulk_soft_delete',
|
|
]
|
|
|
|
@admin.display(description='Keyword')
|
|
def get_keyword(self, obj):
|
|
"""Get keyword from seed_keyword"""
|
|
return obj.seed_keyword.keyword if obj.seed_keyword else '-'
|
|
|
|
@admin.display(description='Volume')
|
|
def get_volume(self, obj):
|
|
"""Get volume from override or seed_keyword"""
|
|
if obj.volume_override is not None:
|
|
return obj.volume_override
|
|
return obj.seed_keyword.volume if obj.seed_keyword else 0
|
|
|
|
@admin.display(description='Difficulty')
|
|
def get_difficulty(self, obj):
|
|
"""Get difficulty from override or seed_keyword"""
|
|
if obj.difficulty_override is not None:
|
|
return obj.difficulty_override
|
|
return obj.seed_keyword.difficulty if obj.seed_keyword else 0
|
|
|
|
@admin.display(description='Country')
|
|
def get_country(self, obj):
|
|
"""Get country from seed_keyword"""
|
|
return obj.seed_keyword.country if obj.seed_keyword else 'US'
|
|
|
|
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'
|
|
|
|
def bulk_assign_cluster(self, request, queryset):
|
|
"""Assign selected keywords to a cluster"""
|
|
from django import forms
|
|
|
|
# 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} keyword(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
|
|
return
|
|
|
|
# Get first keyword's site/sector for filtering clusters
|
|
first_keyword = queryset.first()
|
|
if first_keyword:
|
|
clusters = Clusters.objects.filter(site=first_keyword.site, sector=first_keyword.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 keyword(s) to:"
|
|
)
|
|
|
|
if clusters.exists():
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Assign Keywords to Cluster',
|
|
'queryset': queryset,
|
|
'form': ClusterForm(),
|
|
'action': 'bulk_assign_cluster',
|
|
})
|
|
else:
|
|
self.message_user(request, 'No clusters available for the selected keywords.', messages.WARNING)
|
|
bulk_assign_cluster.short_description = 'Assign to Cluster'
|
|
|
|
def bulk_set_status_active(self, request, queryset):
|
|
"""Set selected keywords to active status"""
|
|
updated = queryset.update(status='active')
|
|
self.message_user(request, f'{updated} keyword(s) set to active.', messages.SUCCESS)
|
|
bulk_set_status_active.short_description = 'Set status to Active'
|
|
|
|
def bulk_set_status_inactive(self, request, queryset):
|
|
"""Set selected keywords to inactive status"""
|
|
updated = queryset.update(status='inactive')
|
|
self.message_user(request, f'{updated} keyword(s) set to inactive.', messages.SUCCESS)
|
|
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
|
|
|
def bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected keywords"""
|
|
count = 0
|
|
for keyword in queryset:
|
|
keyword.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} keyword(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected keywords'
|
|
|
|
|
|
class ContentIdeasResource(resources.ModelResource):
|
|
"""Resource class for importing/exporting Content Ideas"""
|
|
class Meta:
|
|
model = ContentIdeas
|
|
fields = ('id', 'idea_title', 'description', 'site__name', 'sector__name',
|
|
'content_type', 'content_structure', 'status', 'keyword_cluster__name',
|
|
'target_keywords', 'estimated_word_count', 'created_at')
|
|
export_order = fields
|
|
import_id_fields = ('id',)
|
|
skip_unchanged = True
|
|
|
|
|
|
@admin.register(ContentIdeas)
|
|
class ContentIdeasAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
|
resource_class = ContentIdeasResource
|
|
list_display = ['idea_title', 'site', 'sector', 'description_preview', 'content_type', 'content_structure', 'status', 'keyword_cluster', 'estimated_word_count', 'created_at']
|
|
list_select_related = ['site', 'sector', 'keyword_cluster', 'account']
|
|
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector', 'keyword_cluster', 'estimated_word_count', 'created_at']
|
|
search_fields = ['idea_title', 'target_keywords', 'description']
|
|
ordering = ['-created_at']
|
|
readonly_fields = ['created_at', 'updated_at']
|
|
actions = [
|
|
'bulk_set_status_draft',
|
|
'bulk_set_status_approved',
|
|
'bulk_set_status_rejected',
|
|
'bulk_set_status_completed',
|
|
'bulk_assign_cluster',
|
|
'bulk_update_content_type',
|
|
'bulk_soft_delete',
|
|
]
|
|
|
|
fieldsets = (
|
|
('Basic Info', {
|
|
'fields': ('idea_title', 'description', 'status', 'site', 'sector')
|
|
}),
|
|
('Content Planning', {
|
|
'fields': ('content_type', 'content_structure', 'estimated_word_count')
|
|
}),
|
|
('Keywords & Clustering', {
|
|
'fields': ('keyword_cluster', 'target_keywords')
|
|
}),
|
|
('Timestamps', {
|
|
'fields': ('created_at', 'updated_at'),
|
|
'classes': ('collapse',)
|
|
}),
|
|
)
|
|
|
|
def description_preview(self, obj):
|
|
"""Show a truncated preview of the description"""
|
|
if not obj.description:
|
|
return '-'
|
|
# Truncate to 100 characters
|
|
preview = obj.description[:100]
|
|
if len(obj.description) > 100:
|
|
preview += '...'
|
|
return preview
|
|
description_preview.short_description = 'Description'
|
|
|
|
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_keyword_cluster_display(self, obj):
|
|
"""Safely get cluster name"""
|
|
try:
|
|
return obj.keyword_cluster.name if obj.keyword_cluster else '-'
|
|
except:
|
|
return '-'
|
|
get_keyword_cluster_display.short_description = 'Cluster'
|
|
|
|
def bulk_set_status_draft(self, request, queryset):
|
|
"""Set selected content ideas to draft status"""
|
|
updated = queryset.update(status='draft')
|
|
self.message_user(request, f'{updated} content idea(s) set to draft.', messages.SUCCESS)
|
|
bulk_set_status_draft.short_description = 'Set status to Draft'
|
|
|
|
def bulk_set_status_approved(self, request, queryset):
|
|
"""Set selected content ideas to approved status"""
|
|
updated = queryset.update(status='approved')
|
|
self.message_user(request, f'{updated} content idea(s) set to approved.', messages.SUCCESS)
|
|
bulk_set_status_approved.short_description = 'Set status to Approved'
|
|
|
|
def bulk_set_status_rejected(self, request, queryset):
|
|
"""Set selected content ideas to rejected status"""
|
|
updated = queryset.update(status='rejected')
|
|
self.message_user(request, f'{updated} content idea(s) set to rejected.', messages.SUCCESS)
|
|
bulk_set_status_rejected.short_description = 'Set status to Rejected'
|
|
|
|
def bulk_set_status_completed(self, request, queryset):
|
|
"""Set selected content ideas to completed status"""
|
|
updated = queryset.update(status='completed')
|
|
self.message_user(request, f'{updated} content idea(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 content ideas to a cluster"""
|
|
from django import forms
|
|
|
|
if 'apply' in request.POST:
|
|
cluster_id = request.POST.get('cluster')
|
|
if cluster_id:
|
|
cluster = Clusters.objects.get(pk=cluster_id)
|
|
updated = queryset.update(keyword_cluster=cluster)
|
|
self.message_user(request, f'{updated} content idea(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
|
|
return
|
|
|
|
first_idea = queryset.first()
|
|
if first_idea:
|
|
clusters = Clusters.objects.filter(site=first_idea.site, sector=first_idea.sector)
|
|
else:
|
|
clusters = Clusters.objects.all()
|
|
|
|
class ClusterForm(forms.Form):
|
|
cluster = forms.ModelChoiceField(
|
|
queryset=clusters,
|
|
label="Select Cluster",
|
|
help_text=f"Assign {queryset.count()} selected content idea(s) to:"
|
|
)
|
|
|
|
if clusters.exists():
|
|
from django.shortcuts import render
|
|
return render(request, 'admin/bulk_action_form.html', {
|
|
'title': 'Assign Content Ideas to Cluster',
|
|
'queryset': queryset,
|
|
'form': ClusterForm(),
|
|
'action': 'bulk_assign_cluster',
|
|
})
|
|
else:
|
|
self.message_user(request, 'No clusters available for the selected content ideas.', messages.WARNING)
|
|
bulk_assign_cluster.short_description = 'Assign to Cluster'
|
|
|
|
def bulk_update_content_type(self, request, queryset):
|
|
"""Update content type for selected content ideas"""
|
|
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} content idea(s) updated to content type: {content_type}', messages.SUCCESS)
|
|
return
|
|
|
|
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 content idea(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 bulk_soft_delete(self, request, queryset):
|
|
"""Soft delete selected content ideas"""
|
|
count = 0
|
|
for idea in queryset:
|
|
idea.delete() # Soft delete via SoftDeletableModel
|
|
count += 1
|
|
self.message_user(request, f'{count} content idea(s) soft deleted.', messages.SUCCESS)
|
|
bulk_soft_delete.short_description = 'Soft delete selected content ideas'
|