from django.contrib import admin from django.contrib import messages from unfold.admin import ModelAdmin from igny8_core.admin.base import SiteSectorAdminMixin from .models import Tasks, Images, Content from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute, ContentTaxonomyRelation, ContentClusterMap from import_export.admin import ExportMixin from import_export import resources class ContentTaxonomyInline(admin.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 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 @admin.register(Tasks) class TasksAdmin(ExportMixin, SiteSectorAdminMixin, ModelAdmin): resource_class = TaskResource 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'] autocomplete_fields = ['cluster', 'site', 'sector'] actions = [ 'bulk_set_status_draft', 'bulk_set_status_in_progress', 'bulk_set_status_completed', 'bulk_assign_cluster', ] 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 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' @admin.register(Images) class ImagesAdmin(SiteSectorAdminMixin, 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'] ordering = ['-id'] # Sort by ID descending (newest first) 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 '-' class ContentResource(resources.ModelResource): """Resource class for 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', 'external_url', 'created_at') export_order = fields @admin.register(Content) class ContentAdmin(ExportMixin, SiteSectorAdminMixin, ModelAdmin): resource_class = ContentResource list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'get_taxonomy_count', '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', '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', ] 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 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 '-' @admin.register(ContentTaxonomy) class ContentTaxonomyAdmin(SiteSectorAdminMixin, ModelAdmin): 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'] 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') @admin.register(ContentAttribute) class ContentAttributeAdmin(SiteSectorAdminMixin, ModelAdmin): 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'] 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') @admin.register(ContentTaxonomyRelation) class ContentTaxonomyRelationAdmin(ModelAdmin): list_display = ['content', 'taxonomy', 'created_at'] search_fields = ['content__title', 'taxonomy__name'] readonly_fields = ['created_at', 'updated_at'] @admin.register(ContentClusterMap) class ContentClusterMapAdmin(SiteSectorAdminMixin, ModelAdmin): 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']