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', 'site_status', 'scheduled_publish_at', 'word_count', 'get_taxonomy_count', 'created_at'] list_filter = ['status', 'site_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', 'site_status_updated_at', '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') }), ('Publishing Status', { 'fields': ('site_status', 'scheduled_publish_at', 'site_status_updated_at'), 'description': 'WordPress/external site publishing status. Managed by automated publishing scheduler.' }), ('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'