diff --git a/backend/igny8_core/admin/base.py b/backend/igny8_core/admin/base.py index 6b11727e..87f6253d 100644 --- a/backend/igny8_core/admin/base.py +++ b/backend/igny8_core/admin/base.py @@ -1,65 +1,11 @@ """ Base Admin Mixins for account and site/sector filtering. - -ADMIN DELETE FIX: -- Admin can delete anything without 500 errors -- Simple delete that just works """ from django.contrib import admin, messages from django.core.exceptions import PermissionDenied from django.db import models, transaction -class AdminDeleteMixin: - """ - Mixin that provides a simple working delete action for admin. - """ - - def get_actions(self, request): - """Replace default delete_selected with simple working version""" - actions = super().get_actions(request) - - # Remove Django's default delete that causes 500 errors - if 'delete_selected' in actions: - del actions['delete_selected'] - - # Add our simple delete action - actions['simple_delete'] = ( - self.__class__.simple_delete, - 'simple_delete', - 'Delete selected items' - ) - - return actions - - def simple_delete(self, request, queryset): - """ - Simple delete that just works. Deletes items one by one with error handling. - """ - success = 0 - errors = [] - - for obj in queryset: - try: - # Get object info before delete - try: - obj_str = str(obj) - except Exception: - obj_str = f'#{obj.pk}' - - # Just delete it - let the model handle soft vs hard delete - obj.delete() - success += 1 - - except Exception as e: - errors.append(f'{obj_str}: {str(e)[:50]}') - - if success: - self.message_user(request, f'Deleted {success} item(s).', messages.SUCCESS) - if errors: - self.message_user(request, f'Failed to delete {len(errors)}: {"; ".join(errors[:3])}', messages.ERROR) - - class AccountAdminMixin: """Mixin for admin classes that need account filtering""" @@ -171,19 +117,16 @@ class SiteSectorAdminMixin: from unfold.admin import ModelAdmin as UnfoldModelAdmin -class Igny8ModelAdmin(AdminDeleteMixin, UnfoldModelAdmin): +class Igny8ModelAdmin(UnfoldModelAdmin): """ Custom ModelAdmin that: - 1. Fixes delete actions (no 500 errors, bypasses PROTECT if needed) - 2. Ensures sidebar_navigation is set correctly on ALL pages - 3. Uses dropdown filters with Apply button + 1. Ensures sidebar_navigation is set correctly on ALL pages + 2. Uses standard Django filters - AdminDeleteMixin provides: - - simple_delete: Safe delete (soft delete if available) """ - # Enable "Apply Filters" button for dropdown filters - list_filter_submit = True + # Standard Django filters + pass def _inject_sidebar_context(self, request, extra_context=None): """Helper to inject custom sidebar into context""" diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index ab98791c..c669a242 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -5,10 +5,6 @@ from django import forms from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from unfold.admin import ModelAdmin, TabularInline -from unfold.contrib.filters.admin import ( - RelatedDropdownFilter, - ChoicesDropdownFilter, -) from simple_history.admin import SimpleHistoryAdmin from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword, PasswordResetToken @@ -132,12 +128,7 @@ class PlanAdmin(ImportExportMixin, Igny8ModelAdmin): resource_class = PlanResource """Plan admin - Global, no account filtering needed""" list_display = ['name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users', 'max_keywords', 'max_ahrefs_queries', 'included_credits', 'is_active', 'is_featured'] - list_filter = [ - ('is_active', ChoicesDropdownFilter), - ('billing_cycle', ChoicesDropdownFilter), - ('is_internal', ChoicesDropdownFilter), - ('is_featured', ChoicesDropdownFilter), - ] + list_filter = ['is_active', 'billing_cycle', 'is_internal', 'is_featured'] search_fields = ['name', 'slug'] readonly_fields = ['created_at'] actions = [ @@ -212,10 +203,7 @@ class AccountAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode resource_class = AccountResource form = AccountAdminForm list_display = ['name', 'slug', 'owner', 'plan', 'status', 'health_indicator', 'credits', 'created_at'] - list_filter = [ - ('status', ChoicesDropdownFilter), - ('plan', RelatedDropdownFilter), - ] + list_filter = ['status', 'plan'] search_fields = ['name', 'slug'] readonly_fields = ['created_at', 'updated_at', 'health_indicator', 'health_details'] actions = [ @@ -515,9 +503,7 @@ class SubscriptionResource(resources.ModelResource): class SubscriptionAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin): resource_class = SubscriptionResource list_display = ['account', 'status', 'current_period_start', 'current_period_end'] - list_filter = [ - ('status', ChoicesDropdownFilter), - ] + list_filter = ['status'] search_fields = ['account__name', 'stripe_subscription_id'] readonly_fields = ['created_at', 'updated_at'] actions = [ @@ -635,13 +621,7 @@ class SiteResource(resources.ModelResource): class SiteAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin): resource_class = SiteResource list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_api_key_status', 'get_sectors_count'] - list_filter = [ - ('status', ChoicesDropdownFilter), - ('is_active', ChoicesDropdownFilter), - ('account', RelatedDropdownFilter), - ('industry', RelatedDropdownFilter), - ('hosting_type', ChoicesDropdownFilter), - ] + list_filter = ['status', 'is_active', 'account', 'industry', 'hosting_type'] search_fields = ['name', 'slug', 'domain', 'industry__name'] readonly_fields = ['created_at', 'updated_at', 'get_api_key_display'] inlines = [SectorInline] @@ -784,12 +764,7 @@ class SectorResource(resources.ModelResource): class SectorAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin): resource_class = SectorResource list_display = ['name', 'slug', 'site', 'industry_sector', 'get_industry', 'status', 'is_active', 'get_keywords_count', 'get_clusters_count'] - list_filter = [ - ('status', ChoicesDropdownFilter), - ('is_active', ChoicesDropdownFilter), - ('site', RelatedDropdownFilter), - ('industry_sector__industry', RelatedDropdownFilter), - ] + list_filter = ['status', 'is_active', 'site', 'industry_sector__industry'] search_fields = ['name', 'slug', 'site__name', 'industry_sector__name'] readonly_fields = ['created_at', 'updated_at'] actions = [ @@ -1006,12 +981,7 @@ class SeedKeywordAdmin(ImportExportMixin, Igny8ModelAdmin): resource_class = SeedKeywordResource """SeedKeyword admin - Global reference data, no account filtering""" list_display = ['keyword', 'industry', 'sector', 'volume', 'difficulty', 'country', 'is_active', 'created_at'] - list_filter = [ - ('is_active', ChoicesDropdownFilter), - ('industry', RelatedDropdownFilter), - ('sector', RelatedDropdownFilter), - ('country', ChoicesDropdownFilter), - ] + list_filter = ['is_active', 'industry', 'sector', 'country'] search_fields = ['keyword'] readonly_fields = ['created_at', 'updated_at'] actions = [ @@ -1019,7 +989,6 @@ class SeedKeywordAdmin(ImportExportMixin, Igny8ModelAdmin): 'bulk_deactivate', 'bulk_update_country', ] - # Delete is handled by AdminDeleteMixin in base Igny8ModelAdmin fieldsets = ( ('Keyword Info', { @@ -1119,12 +1088,7 @@ class UserAdmin(ExportMixin, BaseUserAdmin, Igny8ModelAdmin): """ resource_class = UserResource list_display = ['email', 'username', 'account', 'role', 'is_active', 'is_staff', 'created_at'] - list_filter = [ - ('role', ChoicesDropdownFilter), - ('account', RelatedDropdownFilter), - ('is_active', ChoicesDropdownFilter), - ('is_staff', ChoicesDropdownFilter), - ] + list_filter = ['role', 'account', 'is_active', 'is_staff'] search_fields = ['email', 'username'] readonly_fields = ['created_at', 'updated_at', 'password_display'] diff --git a/backend/igny8_core/modules/billing/admin.py b/backend/igny8_core/modules/billing/admin.py index 219a0e93..566c0b1d 100644 --- a/backend/igny8_core/modules/billing/admin.py +++ b/backend/igny8_core/modules/billing/admin.py @@ -5,12 +5,6 @@ from django.contrib import admin from django.utils.html import format_html from django.contrib import messages from unfold.admin import ModelAdmin -from unfold.contrib.filters.admin import ( - RelatedDropdownFilter, - ChoicesDropdownFilter, - DropdownFilter, - RangeDateFilter, -) from simple_history.admin import SimpleHistoryAdmin from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin from igny8_core.business.billing.models import ( @@ -41,11 +35,7 @@ class CreditTransactionResource(resources.ModelResource): class CreditTransactionAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin): resource_class = CreditTransactionResource list_display = ['id', 'account', 'transaction_type', 'amount', 'balance_after', 'description', 'created_at'] - list_filter = [ - ('transaction_type', ChoicesDropdownFilter), - ('created_at', RangeDateFilter), - ('account', RelatedDropdownFilter), - ] + list_filter = ['transaction_type', 'created_at', 'account'] search_fields = ['description', 'account__name'] readonly_fields = ['created_at'] date_hierarchy = 'created_at' @@ -197,13 +187,7 @@ class PaymentAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode 'approved_by', 'processed_at', ] - list_filter = [ - ('status', ChoicesDropdownFilter), - ('payment_method', ChoicesDropdownFilter), - ('currency', ChoicesDropdownFilter), - ('created_at', RangeDateFilter), - ('processed_at', RangeDateFilter), - ] + list_filter = ['status', 'payment_method', 'currency', 'created_at', 'processed_at'] search_fields = [ 'invoice__invoice_number', 'account__name', @@ -668,12 +652,7 @@ class PlanLimitUsageAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin): 'period_display', 'created_at', ] - list_filter = [ - ('limit_type', ChoicesDropdownFilter), - ('period_start', RangeDateFilter), - ('period_end', RangeDateFilter), - ('account', RelatedDropdownFilter), - ] + list_filter = ['limit_type', 'period_start', 'period_end', 'account'] search_fields = ['account__name'] readonly_fields = ['created_at', 'updated_at'] date_hierarchy = 'period_start' diff --git a/backend/igny8_core/modules/planner/admin.py b/backend/igny8_core/modules/planner/admin.py index 06956483..460c8372 100644 --- a/backend/igny8_core/modules/planner/admin.py +++ b/backend/igny8_core/modules/planner/admin.py @@ -1,12 +1,6 @@ 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 @@ -40,13 +34,7 @@ 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), - ] + list_filter = ['status', 'site', 'sector', 'volume', 'created_at'] search_fields = ['name'] ordering = ['name'] autocomplete_fields = ['site', 'sector'] @@ -100,13 +88,7 @@ class KeywordsAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin): 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), - ] + list_filter = ['status', 'site', 'sector', 'cluster', 'created_at'] search_fields = ['seed_keyword__keyword'] ordering = ['-created_at'] autocomplete_fields = ['cluster', 'site', 'sector', 'seed_keyword'] @@ -114,6 +96,7 @@ class KeywordsAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin): 'bulk_assign_cluster', 'bulk_set_status_active', 'bulk_set_status_inactive', + 'bulk_soft_delete', ] @admin.display(description='Keyword') @@ -242,16 +225,7 @@ 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), - ] + 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'] diff --git a/backend/igny8_core/modules/writer/admin.py b/backend/igny8_core/modules/writer/admin.py index 82c39fb1..28a5b70b 100644 --- a/backend/igny8_core/modules/writer/admin.py +++ b/backend/igny8_core/modules/writer/admin.py @@ -1,12 +1,6 @@ from django.contrib import admin from django.contrib import messages from unfold.admin import ModelAdmin, TabularInline -from unfold.contrib.filters.admin import ( - RangeDateFilter, - RangeNumericFilter, - RelatedDropdownFilter, - ChoicesDropdownFilter, -) 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 @@ -39,15 +33,7 @@ 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', ChoicesDropdownFilter), - ('content_type', ChoicesDropdownFilter), - ('content_structure', ChoicesDropdownFilter), - ('site', RelatedDropdownFilter), - ('sector', RelatedDropdownFilter), - ('cluster', RelatedDropdownFilter), - ('created_at', RangeDateFilter), - ] + 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'] @@ -315,13 +301,7 @@ class ImagePromptsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin): resource_class = ImagePromptsResource list_display = ['get_content_title', 'site', 'sector', 'image_type', 'get_prompt_preview', 'status', 'created_at'] - list_filter = [ - ('image_type', ChoicesDropdownFilter), - ('status', ChoicesDropdownFilter), - ('site', RelatedDropdownFilter), - ('sector', RelatedDropdownFilter), - ('created_at', RangeDateFilter), - ] + 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', @@ -450,17 +430,7 @@ class ContentResource(resources.ModelResource): 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', ChoicesDropdownFilter), - ('content_type', ChoicesDropdownFilter), - ('content_structure', ChoicesDropdownFilter), - ('source', ChoicesDropdownFilter), - ('site', RelatedDropdownFilter), - ('sector', RelatedDropdownFilter), - ('cluster', RelatedDropdownFilter), - ('word_count', RangeNumericFilter), - ('created_at', RangeDateFilter), - ] + 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'] diff --git a/backend/igny8_core/templates/admin/seedkeyword_delete_confirmation.html b/backend/igny8_core/templates/admin/seedkeyword_delete_confirmation.html deleted file mode 100644 index eb4aad8a..00000000 --- a/backend/igny8_core/templates/admin/seedkeyword_delete_confirmation.html +++ /dev/null @@ -1,92 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load i18n l10n admin_urls static %} - -{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation{% endblock %} - -{% block breadcrumbs %} - -{% endblock %} - -{% block content %} -
- {% if subtitle %} -

{{ title }}

-

{{ subtitle }}

- {% else %} -

{{ title }}

- {% endif %} - - {% if can_delete_items %} -
-

✅ Can Delete ({{ can_delete_items|length }} item{{ can_delete_items|length|pluralize }})

-

The following seed keywords can be safely deleted:

- -
- {% endif %} - - {% if protected_items %} -
-

⚠️ Cannot Delete ({{ protected_items|length }} item{{ protected_items|length|pluralize }})

-

The following seed keywords are being used by site keywords and cannot be deleted:

- -

- 💡 Tip: To delete these seed keywords, you must first remove or deactivate the site keywords that reference them. -

-
- {% endif %} - - {% if can_delete_items %} -
- {% csrf_token %} -
- {% for obj in queryset %} - - {% endfor %} - - - -
- - {% trans 'No, take me back' %} -
- - {% if protected_items %} -

- Note: Only the {{ can_delete_items|length }} deletable keyword{{ can_delete_items|length|pluralize }} will be deleted. - The {{ protected_items|length }} protected keyword{{ protected_items|length|pluralize }} will remain unchanged. -

- {% endif %} -
-
- {% else %} -
-

- ⛔ No keywords can be deleted.
- All selected keywords are currently in use by site keywords and are protected from deletion. -

- {% trans 'Back to list' %} -
- {% endif %} -
-{% endblock %}