django bacekdn opeartioanl fixes and site wp integration api fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-11 21:54:08 +00:00
parent 00ef985a5f
commit 3925ddf894
16 changed files with 2045 additions and 89 deletions

View File

@@ -2,14 +2,18 @@
Admin interface for auth models
"""
from django import forms
from django.contrib import admin
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
from import_export.admin import ExportMixin, ImportExportMixin
from import_export import resources
from import_export import resources, fields, widgets
class AccountAdminForm(forms.ModelForm):
@@ -128,7 +132,12 @@ 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', 'billing_cycle', 'is_internal', 'is_featured']
list_filter = [
('is_active', ChoicesDropdownFilter),
('billing_cycle', ChoicesDropdownFilter),
('is_internal', ChoicesDropdownFilter),
('is_featured', ChoicesDropdownFilter),
]
search_fields = ['name', 'slug']
readonly_fields = ['created_at']
actions = [
@@ -203,7 +212,10 @@ 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', 'plan']
list_filter = [
('status', ChoicesDropdownFilter),
('plan', RelatedDropdownFilter),
]
search_fields = ['name', 'slug']
readonly_fields = ['created_at', 'updated_at', 'health_indicator', 'health_details']
actions = [
@@ -503,7 +515,9 @@ 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']
list_filter = [
('status', ChoicesDropdownFilter),
]
search_fields = ['account__name', 'stripe_subscription_id']
readonly_fields = ['created_at', 'updated_at']
actions = [
@@ -621,7 +635,13 @@ 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', 'is_active', 'account', 'industry', 'hosting_type']
list_filter = [
('status', ChoicesDropdownFilter),
('is_active', ChoicesDropdownFilter),
('account', RelatedDropdownFilter),
('industry', RelatedDropdownFilter),
('hosting_type', ChoicesDropdownFilter),
]
search_fields = ['name', 'slug', 'domain', 'industry__name']
readonly_fields = ['created_at', 'updated_at', 'get_api_key_display']
inlines = [SectorInline]
@@ -676,15 +696,36 @@ class SiteAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
get_api_key_status.short_description = 'API Key'
def generate_api_keys(self, request, queryset):
"""Generate API keys for selected sites"""
"""Generate API keys for selected sites. API key is stored ONLY in Site.wp_api_key (single source of truth)."""
import secrets
from igny8_core.business.integration.models import SiteIntegration
updated_count = 0
for site in queryset:
if not site.wp_api_key:
site.wp_api_key = f"igny8_{''.join(secrets.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(40))}"
api_key = f"igny8_{''.join(secrets.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(40))}"
# SINGLE SOURCE OF TRUTH: Store API key ONLY in Site.wp_api_key
site.wp_api_key = api_key
site.save()
# Ensure SiteIntegration exists for status tracking (without API key)
SiteIntegration.objects.get_or_create(
site=site,
platform='wordpress',
defaults={
'account': site.account,
'platform': 'wordpress',
'platform_type': 'cms',
'is_active': True,
'sync_enabled': True,
'credentials_json': {}, # Empty - API key is on Site model
'config_json': {}
}
)
updated_count += 1
self.message_user(request, f'Generated API keys for {updated_count} site(s). Sites with existing keys were skipped.')
self.message_user(request, f'Generated API keys for {updated_count} site(s). API keys stored in Site.wp_api_key (single source of truth).')
generate_api_keys.short_description = 'Generate WordPress API Keys'
def bulk_set_status_active(self, request, queryset):
@@ -743,7 +784,12 @@ 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', 'is_active', 'site', 'industry_sector__industry']
list_filter = [
('status', ChoicesDropdownFilter),
('is_active', ChoicesDropdownFilter),
('site', RelatedDropdownFilter),
('industry_sector__industry', RelatedDropdownFilter),
]
search_fields = ['name', 'slug', 'site__name', 'industry_sector__name']
readonly_fields = ['created_at', 'updated_at']
actions = [
@@ -877,7 +923,10 @@ class IndustrySectorResource(resources.ModelResource):
class IndustrySectorAdmin(ImportExportMixin, Igny8ModelAdmin):
resource_class = IndustrySectorResource
list_display = ['name', 'slug', 'industry', 'is_active']
list_filter = ['is_active', 'industry']
list_filter = [
('is_active', ChoicesDropdownFilter),
('industry', RelatedDropdownFilter),
]
search_fields = ['name', 'slug', 'description']
readonly_fields = ['created_at', 'updated_at']
actions = [
@@ -903,13 +952,53 @@ class IndustrySectorAdmin(ImportExportMixin, Igny8ModelAdmin):
class SeedKeywordResource(resources.ModelResource):
"""Resource class for importing/exporting Seed Keywords"""
industry = fields.Field(
column_name='industry',
attribute='industry',
widget=widgets.ForeignKeyWidget(Industry, 'name')
)
sector = fields.Field(
column_name='sector',
attribute='sector',
widget=widgets.ForeignKeyWidget(IndustrySector, 'name')
)
class Meta:
model = SeedKeyword
fields = ('id', 'keyword', 'industry__name', 'sector__name', 'volume',
fields = ('id', 'keyword', 'industry', 'sector', 'volume',
'difficulty', 'country', 'is_active', 'created_at')
export_order = fields
import_id_fields = ('id',)
import_id_fields = ('keyword', 'industry', 'sector') # Use natural keys for import
skip_unchanged = True
def before_import_row(self, row, **kwargs):
"""Clean and validate row data before import"""
# Ensure volume is an integer
if 'volume' in row:
try:
row['volume'] = int(row['volume']) if row['volume'] else 0
except (ValueError, TypeError):
row['volume'] = 0
# Ensure difficulty is an integer between 0-100
if 'difficulty' in row:
try:
difficulty = int(row['difficulty']) if row['difficulty'] else 0
row['difficulty'] = max(0, min(100, difficulty)) # Clamp to 0-100
except (ValueError, TypeError):
row['difficulty'] = 0
# Ensure country is valid
if 'country' in row:
valid_countries = [code for code, name in SeedKeyword.COUNTRY_CHOICES]
if row['country'] not in valid_countries:
row['country'] = 'US' # Default to US if invalid
# Set defaults for optional fields
if 'is_active' not in row or row['is_active'] == '':
row['is_active'] = True
return row
@admin.register(SeedKeyword)
@@ -917,15 +1006,20 @@ 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', 'industry', 'sector', 'country']
list_filter = [
('is_active', ChoicesDropdownFilter),
('industry', RelatedDropdownFilter),
('sector', RelatedDropdownFilter),
('country', ChoicesDropdownFilter),
]
search_fields = ['keyword']
readonly_fields = ['created_at', 'updated_at']
actions = [
'delete_selected',
'bulk_activate',
'bulk_deactivate',
'bulk_update_country',
] # Enable bulk delete
]
# Delete is handled by AdminDeleteMixin in base Igny8ModelAdmin
fieldsets = (
('Keyword Info', {
@@ -939,18 +1033,38 @@ class SeedKeywordAdmin(ImportExportMixin, Igny8ModelAdmin):
}),
)
def has_delete_permission(self, request, obj=None):
"""Allow deletion for superusers and developers"""
return request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer())
def bulk_activate(self, request, queryset):
updated = queryset.update(is_active=True)
self.message_user(request, f'{updated} seed keyword(s) activated.', messages.SUCCESS)
"""Activate selected keywords"""
try:
updated = queryset.update(is_active=True)
self.message_user(
request,
f'{updated} seed keyword(s) activated successfully.',
messages.SUCCESS
)
except Exception as e:
self.message_user(
request,
f'Error activating keywords: {str(e)}',
messages.ERROR
)
bulk_activate.short_description = 'Activate selected keywords'
def bulk_deactivate(self, request, queryset):
updated = queryset.update(is_active=False)
self.message_user(request, f'{updated} seed keyword(s) deactivated.', messages.SUCCESS)
"""Deactivate selected keywords"""
try:
updated = queryset.update(is_active=False)
self.message_user(
request,
f'{updated} seed keyword(s) deactivated successfully.',
messages.SUCCESS
)
except Exception as e:
self.message_user(
request,
f'Error deactivating keywords: {str(e)}',
messages.ERROR
)
bulk_deactivate.short_description = 'Deactivate selected keywords'
def bulk_update_country(self, request, queryset):
@@ -1005,7 +1119,12 @@ class UserAdmin(ExportMixin, BaseUserAdmin, Igny8ModelAdmin):
"""
resource_class = UserResource
list_display = ['email', 'username', 'account', 'role', 'is_active', 'is_staff', 'created_at']
list_filter = ['role', 'account', 'is_active', 'is_staff']
list_filter = [
('role', ChoicesDropdownFilter),
('account', RelatedDropdownFilter),
('is_active', ChoicesDropdownFilter),
('is_staff', ChoicesDropdownFilter),
]
search_fields = ['email', 'username']
readonly_fields = ['created_at', 'updated_at', 'password_display']