django bacekdn opeartioanl fixes and site wp integration api fixes
This commit is contained in:
@@ -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']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user