latest 2 days work excluding problemetic code

This commit is contained in:
alorig
2026-01-12 14:23:05 +05:00
parent b390e02aa5
commit e9f02f5e9f
6 changed files with 22 additions and 284 deletions

View File

@@ -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"""

View File

@@ -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']

View File

@@ -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'

View File

@@ -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']

View File

@@ -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']

View File

@@ -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 %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; {% trans 'Delete multiple objects' %}
</div>
{% endblock %}
{% block content %}
<div class="delete-confirmation">
{% if subtitle %}
<h1>{{ title }}</h1>
<p class="subtitle">{{ subtitle }}</p>
{% else %}
<h1>{{ title }}</h1>
{% endif %}
{% if can_delete_items %}
<div class="can-delete-section" style="margin: 20px 0; padding: 15px; background: #e8f5e9; border-left: 4px solid #4caf50;">
<h3 style="margin-top: 0; color: #2e7d32;">✅ Can Delete ({{ can_delete_items|length }} item{{ can_delete_items|length|pluralize }})</h3>
<p>The following seed keywords can be safely deleted:</p>
<ul>
{% for obj in can_delete_items %}
<li><strong>{{ obj.keyword }}</strong> ({{ obj.industry.name }} - {{ obj.sector.name }})</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if protected_items %}
<div class="protected-section" style="margin: 20px 0; padding: 15px; background: #fff3e0; border-left: 4px solid #ff9800;">
<h3 style="margin-top: 0; color: #e65100;">⚠️ Cannot Delete ({{ protected_items|length }} item{{ protected_items|length|pluralize }})</h3>
<p>The following seed keywords are being used by site keywords and <strong>cannot be deleted</strong>:</p>
<ul>
{% for item in protected_items %}
<li>
<strong>{{ item.object.keyword }}</strong> ({{ item.object.industry.name }} - {{ item.object.sector.name }})
<br>
<span style="color: #666; font-size: 0.9em;">
→ Used by <strong>{{ item.related_count }}</strong> keyword{{ item.related_count|pluralize }} on sites:
{% for site in item.sites %}{{ site }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% if item.related_count > 5 %}(+{{ item.related_count|add:"-5" }} more){% endif %}
</span>
</li>
{% endfor %}
</ul>
<p style="margin-top: 15px; padding: 10px; background: #fff; border: 1px solid #ff9800;">
<strong>💡 Tip:</strong> To delete these seed keywords, you must first remove or deactivate the site keywords that reference them.
</p>
</div>
{% endif %}
{% if can_delete_items %}
<form method="post">
{% csrf_token %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}">
{% endfor %}
<input type="hidden" name="action" value="{{ action }}">
<input type="hidden" name="post" value="yes">
<div style="margin: 20px 0;">
<input type="submit" value="{% trans 'Yes, delete selected items' %}" class="button" style="background: #dc3545; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">
<a href="{% url opts|admin_urlname:'changelist' %}" class="button cancel-link" style="margin-left: 10px; padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;">{% trans 'No, take me back' %}</a>
</div>
{% if protected_items %}
<p style="color: #e65100;">
<strong>Note:</strong> 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.
</p>
{% endif %}
</div>
</form>
{% else %}
<div style="margin: 20px 0;">
<p style="background: #ffebee; padding: 15px; border-left: 4px solid #f44336;">
<strong>⛔ No keywords can be deleted.</strong><br>
All selected keywords are currently in use by site keywords and are protected from deletion.
</p>
<a href="{% url opts|admin_urlname:'changelist' %}" class="button" style="padding: 10px 20px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px; display: inline-block;">{% trans 'Back to list' %}</a>
</div>
{% endif %}
</div>
{% endblock %}