from django.contrib import admin from django.contrib import messages from unfold.admin import ModelAdmin from unfold.contrib.filters.admin import ( RangeDateFilter, RangeNumericFilter, RelatedDropdownFilter, ChoicesDropdownFilter, ) 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', ChoicesDropdownFilter), ('site', RelatedDropdownFilter), ('sector', RelatedDropdownFilter), ('volume', RangeNumericFilter), ('created_at', RangeDateFilter), ] 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', ChoicesDropdownFilter), ('site', RelatedDropdownFilter), ('sector', RelatedDropdownFilter), ('cluster', RelatedDropdownFilter), ('created_at', RangeDateFilter), ] 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', ] @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', ChoicesDropdownFilter), ('content_type', ChoicesDropdownFilter), ('content_structure', ChoicesDropdownFilter), ('site', RelatedDropdownFilter), ('sector', RelatedDropdownFilter), ('keyword_cluster', RelatedDropdownFilter), ('estimated_word_count', RangeNumericFilter), ('created_at', RangeDateFilter), ] 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'