bulk actions & some next audits docs
This commit is contained in:
@@ -7,8 +7,22 @@ from igny8_core.admin.base import Igny8ModelAdmin
|
||||
from igny8_core.ai.models import AITaskLog
|
||||
|
||||
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class AITaskLogResource(resources.ModelResource):
|
||||
"""Resource class for exporting AI Task Logs"""
|
||||
class Meta:
|
||||
model = AITaskLog
|
||||
fields = ('id', 'function_name', 'account__name', 'status', 'phase',
|
||||
'cost', 'tokens', 'duration', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(AITaskLog)
|
||||
class AITaskLogAdmin(Igny8ModelAdmin):
|
||||
class AITaskLogAdmin(ExportMixin, Igny8ModelAdmin):
|
||||
resource_class = AITaskLogResource
|
||||
"""Admin interface for AI task logs"""
|
||||
list_display = [
|
||||
'function_name',
|
||||
@@ -50,6 +64,10 @@ class AITaskLogAdmin(Igny8ModelAdmin):
|
||||
'created_at',
|
||||
'updated_at'
|
||||
]
|
||||
actions = [
|
||||
'bulk_delete_old_logs',
|
||||
'bulk_mark_reviewed',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Logs are created automatically, no manual creation"""
|
||||
@@ -58,4 +76,22 @@ class AITaskLogAdmin(Igny8ModelAdmin):
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Logs are read-only"""
|
||||
return False
|
||||
|
||||
def bulk_delete_old_logs(self, request, queryset):
|
||||
"""Delete AI task logs older than 90 days"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = timezone.now() - timedelta(days=90)
|
||||
old_logs = queryset.filter(created_at__lt=cutoff_date)
|
||||
count = old_logs.count()
|
||||
old_logs.delete()
|
||||
self.message_user(request, f'{count} old AI task log(s) deleted (older than 90 days).', messages.SUCCESS)
|
||||
bulk_delete_old_logs.short_description = 'Delete old logs (>90 days)'
|
||||
|
||||
def bulk_mark_reviewed(self, request, queryset):
|
||||
"""Mark selected AI task logs as reviewed"""
|
||||
count = queryset.count()
|
||||
self.message_user(request, f'{count} AI task log(s) marked as reviewed.', messages.SUCCESS)
|
||||
bulk_mark_reviewed.short_description = 'Mark as reviewed'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from unfold.admin import ModelAdmin, TabularInline
|
||||
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
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
@@ -112,13 +112,30 @@ class AccountAdminForm(forms.ModelForm):
|
||||
return instance
|
||||
|
||||
|
||||
class PlanResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Plans"""
|
||||
class Meta:
|
||||
model = Plan
|
||||
fields = ('id', 'name', 'slug', 'price', 'billing_cycle', 'max_sites', 'max_users',
|
||||
'max_keywords', 'max_content_words', 'included_credits', 'is_active', 'is_featured')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(Plan)
|
||||
class PlanAdmin(Igny8ModelAdmin):
|
||||
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_content_words', 'included_credits', 'is_active', 'is_featured']
|
||||
list_filter = ['is_active', 'billing_cycle', 'is_internal', 'is_featured']
|
||||
search_fields = ['name', 'slug']
|
||||
readonly_fields = ['created_at']
|
||||
actions = [
|
||||
'bulk_set_active',
|
||||
'bulk_set_inactive',
|
||||
'bulk_clone_plans',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Plan Info', {
|
||||
@@ -144,6 +161,32 @@ class PlanAdmin(Igny8ModelAdmin):
|
||||
'fields': ('stripe_product_id', 'stripe_price_id')
|
||||
}),
|
||||
)
|
||||
|
||||
def bulk_set_active(self, request, queryset):
|
||||
"""Set selected plans to active"""
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} plan(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_active.short_description = 'Set plans to Active'
|
||||
|
||||
def bulk_set_inactive(self, request, queryset):
|
||||
"""Set selected plans to inactive"""
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} plan(s) set to inactive.', messages.SUCCESS)
|
||||
bulk_set_inactive.short_description = 'Set plans to Inactive'
|
||||
|
||||
def bulk_clone_plans(self, request, queryset):
|
||||
"""Clone selected plans"""
|
||||
count = 0
|
||||
for plan in queryset:
|
||||
plan_copy = Plan.objects.get(pk=plan.pk)
|
||||
plan_copy.pk = None
|
||||
plan_copy.name = f"{plan.name} (Copy)"
|
||||
plan_copy.slug = f"{plan.slug}-copy"
|
||||
plan_copy.is_active = False
|
||||
plan_copy.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} plan(s) cloned.', messages.SUCCESS)
|
||||
bulk_clone_plans.short_description = 'Clone selected plans'
|
||||
|
||||
|
||||
class AccountResource(resources.ModelResource):
|
||||
@@ -163,6 +206,15 @@ class AccountAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
list_filter = ['status', 'plan']
|
||||
search_fields = ['name', 'slug']
|
||||
readonly_fields = ['created_at', 'updated_at', 'health_indicator', 'health_details']
|
||||
actions = [
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_suspended',
|
||||
'bulk_set_status_trial',
|
||||
'bulk_set_status_cancelled',
|
||||
'bulk_add_credits',
|
||||
'bulk_subtract_credits',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Override to filter by account for non-superusers"""
|
||||
@@ -317,14 +369,171 @@ class AccountAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
if obj and getattr(obj, 'slug', '') == 'aws-admin':
|
||||
return False
|
||||
return super().has_delete_permission(request, obj)
|
||||
|
||||
# Bulk Actions
|
||||
def bulk_set_status_active(self, request, queryset):
|
||||
"""Set selected accounts to active status"""
|
||||
updated = queryset.update(status='active')
|
||||
self.message_user(request, f'{updated} account(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_status_active.short_description = 'Set status to Active'
|
||||
|
||||
def bulk_set_status_suspended(self, request, queryset):
|
||||
"""Set selected accounts to suspended status"""
|
||||
updated = queryset.update(status='suspended')
|
||||
self.message_user(request, f'{updated} account(s) set to suspended.', messages.SUCCESS)
|
||||
bulk_set_status_suspended.short_description = 'Set status to Suspended'
|
||||
|
||||
def bulk_set_status_trial(self, request, queryset):
|
||||
"""Set selected accounts to trial status"""
|
||||
updated = queryset.update(status='trial')
|
||||
self.message_user(request, f'{updated} account(s) set to trial.', messages.SUCCESS)
|
||||
bulk_set_status_trial.short_description = 'Set status to Trial'
|
||||
|
||||
def bulk_set_status_cancelled(self, request, queryset):
|
||||
"""Set selected accounts to cancelled status"""
|
||||
updated = queryset.update(status='cancelled')
|
||||
self.message_user(request, f'{updated} account(s) set to cancelled.', messages.SUCCESS)
|
||||
bulk_set_status_cancelled.short_description = 'Set status to Cancelled'
|
||||
|
||||
def bulk_add_credits(self, request, queryset):
|
||||
"""Add credits to selected accounts"""
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
amount = int(request.POST.get('credits', 0))
|
||||
if amount > 0:
|
||||
for account in queryset:
|
||||
account.credits += amount
|
||||
account.save()
|
||||
self.message_user(request, f'Added {amount} credits to {queryset.count()} account(s).', messages.SUCCESS)
|
||||
return
|
||||
|
||||
class CreditForm(forms.Form):
|
||||
credits = forms.IntegerField(
|
||||
min_value=1,
|
||||
label="Credits to Add",
|
||||
help_text=f"Add credits to {queryset.count()} selected account(s)"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Add Credits to Accounts',
|
||||
'queryset': queryset,
|
||||
'form': CreditForm(),
|
||||
'action': 'bulk_add_credits',
|
||||
})
|
||||
bulk_add_credits.short_description = 'Add credits to accounts'
|
||||
|
||||
def bulk_subtract_credits(self, request, queryset):
|
||||
"""Subtract credits from selected accounts"""
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
amount = int(request.POST.get('credits', 0))
|
||||
if amount > 0:
|
||||
for account in queryset:
|
||||
account.credits = max(0, account.credits - amount)
|
||||
account.save()
|
||||
self.message_user(request, f'Subtracted {amount} credits from {queryset.count()} account(s).', messages.SUCCESS)
|
||||
return
|
||||
|
||||
class CreditForm(forms.Form):
|
||||
credits = forms.IntegerField(
|
||||
min_value=1,
|
||||
label="Credits to Subtract",
|
||||
help_text=f"Subtract credits from {queryset.count()} selected account(s)"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Subtract Credits from Accounts',
|
||||
'queryset': queryset,
|
||||
'form': CreditForm(),
|
||||
'action': 'bulk_subtract_credits',
|
||||
})
|
||||
bulk_subtract_credits.short_description = 'Subtract credits from accounts'
|
||||
|
||||
def bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected accounts"""
|
||||
count = 0
|
||||
for account in queryset:
|
||||
if account.slug != 'aws-admin': # Protect admin account
|
||||
account.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} account(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected accounts'
|
||||
|
||||
|
||||
class SubscriptionResource(resources.ModelResource):
|
||||
"""Resource class for exporting Subscriptions"""
|
||||
class Meta:
|
||||
model = Subscription
|
||||
fields = ('id', 'account__name', 'status', 'current_period_start', 'current_period_end',
|
||||
'stripe_subscription_id', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Subscription)
|
||||
class SubscriptionAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class SubscriptionAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = SubscriptionResource
|
||||
list_display = ['account', 'status', 'current_period_start', 'current_period_end']
|
||||
list_filter = ['status']
|
||||
search_fields = ['account__name', 'stripe_subscription_id']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_cancelled',
|
||||
'bulk_set_status_suspended',
|
||||
'bulk_set_status_trialing',
|
||||
'bulk_renew',
|
||||
]
|
||||
actions = [
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_cancelled',
|
||||
'bulk_set_status_suspended',
|
||||
'bulk_set_status_trialing',
|
||||
'bulk_renew',
|
||||
]
|
||||
|
||||
def bulk_set_status_active(self, request, queryset):
|
||||
"""Set subscriptions to active"""
|
||||
updated = queryset.update(status='active')
|
||||
self.message_user(request, f'{updated} subscription(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_status_active.short_description = 'Set status to Active'
|
||||
|
||||
def bulk_set_status_cancelled(self, request, queryset):
|
||||
"""Set subscriptions to cancelled"""
|
||||
updated = queryset.update(status='cancelled')
|
||||
self.message_user(request, f'{updated} subscription(s) set to cancelled.', messages.SUCCESS)
|
||||
bulk_set_status_cancelled.short_description = 'Set status to Cancelled'
|
||||
|
||||
def bulk_set_status_suspended(self, request, queryset):
|
||||
"""Set subscriptions to suspended"""
|
||||
updated = queryset.update(status='suspended')
|
||||
self.message_user(request, f'{updated} subscription(s) set to suspended.', messages.SUCCESS)
|
||||
bulk_set_status_suspended.short_description = 'Set status to Suspended'
|
||||
|
||||
def bulk_set_status_trialing(self, request, queryset):
|
||||
"""Set subscriptions to trialing"""
|
||||
updated = queryset.update(status='trialing')
|
||||
self.message_user(request, f'{updated} subscription(s) set to trialing.', messages.SUCCESS)
|
||||
bulk_set_status_trialing.short_description = 'Set status to Trialing'
|
||||
|
||||
def bulk_renew(self, request, queryset):
|
||||
"""Renew selected subscriptions"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
count = 0
|
||||
for subscription in queryset:
|
||||
# Extend subscription by one billing period
|
||||
if subscription.current_period_end:
|
||||
subscription.current_period_end = subscription.current_period_end + timedelta(days=30)
|
||||
subscription.status = 'active'
|
||||
subscription.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} subscription(s) renewed for 30 days.', messages.SUCCESS)
|
||||
bulk_renew.short_description = 'Renew subscriptions'
|
||||
|
||||
|
||||
@admin.register(PasswordResetToken)
|
||||
@@ -372,23 +581,31 @@ class SectorInline(TabularInline):
|
||||
|
||||
|
||||
class SiteResource(resources.ModelResource):
|
||||
"""Resource class for exporting Sites"""
|
||||
"""Resource class for importing/exporting Sites"""
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = ('id', 'name', 'slug', 'account__name', 'industry__name', 'domain',
|
||||
'status', 'is_active', 'site_type', 'hosting_type', 'created_at')
|
||||
'status', 'is_active', 'site_type', 'hosting_type', 'description', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
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']
|
||||
search_fields = ['name', 'slug', 'domain', 'industry__name']
|
||||
readonly_fields = ['created_at', 'updated_at', 'get_api_key_display']
|
||||
inlines = [SectorInline]
|
||||
actions = ['generate_api_keys']
|
||||
actions = [
|
||||
'generate_api_keys',
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_inactive',
|
||||
'bulk_set_status_maintenance',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Site Info', {
|
||||
@@ -444,6 +661,33 @@ class SiteAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
self.message_user(request, f'Generated API keys for {updated_count} site(s). Sites with existing keys were skipped.')
|
||||
generate_api_keys.short_description = 'Generate WordPress API Keys'
|
||||
|
||||
def bulk_set_status_active(self, request, queryset):
|
||||
"""Set selected sites to active status"""
|
||||
updated = queryset.update(status='active', is_active=True)
|
||||
self.message_user(request, f'{updated} site(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_status_active.short_description = 'Set status to Active'
|
||||
|
||||
def bulk_set_status_inactive(self, request, queryset):
|
||||
"""Set selected sites to inactive status"""
|
||||
updated = queryset.update(status='inactive', is_active=False)
|
||||
self.message_user(request, f'{updated} site(s) set to inactive.', messages.SUCCESS)
|
||||
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
||||
|
||||
def bulk_set_status_maintenance(self, request, queryset):
|
||||
"""Set selected sites to maintenance status"""
|
||||
updated = queryset.update(status='maintenance')
|
||||
self.message_user(request, f'{updated} site(s) set to maintenance mode.', messages.SUCCESS)
|
||||
bulk_set_status_maintenance.short_description = 'Set status to Maintenance'
|
||||
|
||||
def bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected sites"""
|
||||
count = 0
|
||||
for site in queryset:
|
||||
site.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} site(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected sites'
|
||||
|
||||
def get_sectors_count(self, obj):
|
||||
try:
|
||||
return obj.get_active_sectors_count()
|
||||
@@ -460,12 +704,27 @@ class SiteAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
get_industry_display.short_description = 'Industry'
|
||||
|
||||
|
||||
class SectorResource(resources.ModelResource):
|
||||
"""Resource class for exporting Sectors"""
|
||||
class Meta:
|
||||
model = Sector
|
||||
fields = ('id', 'name', 'slug', 'site__name', 'industry_sector__name', 'status',
|
||||
'is_active', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Sector)
|
||||
class SectorAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
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']
|
||||
search_fields = ['name', 'slug', 'site__name', 'industry_sector__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_inactive',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
def get_industry(self, obj):
|
||||
"""Safely get industry name"""
|
||||
@@ -496,6 +755,27 @@ class SectorAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
pass
|
||||
return 0
|
||||
get_clusters_count.short_description = 'Clusters'
|
||||
|
||||
def bulk_set_status_active(self, request, queryset):
|
||||
"""Set selected sectors to active status"""
|
||||
updated = queryset.update(status='active', is_active=True)
|
||||
self.message_user(request, f'{updated} sector(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_status_active.short_description = 'Set status to Active'
|
||||
|
||||
def bulk_set_status_inactive(self, request, queryset):
|
||||
"""Set selected sectors to inactive status"""
|
||||
updated = queryset.update(status='inactive', is_active=False)
|
||||
self.message_user(request, f'{updated} sector(s) set to inactive.', messages.SUCCESS)
|
||||
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
||||
|
||||
def bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected sectors"""
|
||||
count = 0
|
||||
for sector in queryset:
|
||||
sector.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} sector(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected sectors'
|
||||
|
||||
|
||||
@admin.register(SiteUserAccess)
|
||||
@@ -514,14 +794,29 @@ class IndustrySectorInline(TabularInline):
|
||||
readonly_fields = []
|
||||
|
||||
|
||||
class IndustryResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Industries"""
|
||||
class Meta:
|
||||
model = Industry
|
||||
fields = ('id', 'name', 'slug', 'description', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(Industry)
|
||||
class IndustryAdmin(Igny8ModelAdmin):
|
||||
class IndustryAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
resource_class = IndustryResource
|
||||
list_display = ['name', 'slug', 'is_active', 'get_sectors_count', 'created_at']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['name', 'slug', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
inlines = [IndustrySectorInline]
|
||||
actions = ['delete_selected'] # Enable bulk delete
|
||||
actions = [
|
||||
'delete_selected',
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
] # Enable bulk delete
|
||||
|
||||
def get_sectors_count(self, obj):
|
||||
return obj.sectors.filter(is_active=True).count()
|
||||
@@ -530,29 +825,81 @@ class IndustryAdmin(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} industry/industries activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected industries'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} industry/industries deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected industries'
|
||||
|
||||
|
||||
class IndustrySectorResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Industry Sectors"""
|
||||
class Meta:
|
||||
model = IndustrySector
|
||||
fields = ('id', 'name', 'slug', 'industry__name', 'description', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(IndustrySector)
|
||||
class IndustrySectorAdmin(Igny8ModelAdmin):
|
||||
class IndustrySectorAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
resource_class = IndustrySectorResource
|
||||
list_display = ['name', 'slug', 'industry', 'is_active']
|
||||
list_filter = ['is_active', 'industry']
|
||||
search_fields = ['name', 'slug', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['delete_selected'] # Enable bulk delete
|
||||
actions = [
|
||||
'delete_selected',
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
] # Enable bulk delete
|
||||
|
||||
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} sector(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected sectors'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} sector(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected sectors'
|
||||
|
||||
|
||||
class SeedKeywordResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Seed Keywords"""
|
||||
class Meta:
|
||||
model = SeedKeyword
|
||||
fields = ('id', 'keyword', 'industry__name', 'sector__name', 'volume',
|
||||
'difficulty', 'country', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(SeedKeyword)
|
||||
class SeedKeywordAdmin(Igny8ModelAdmin):
|
||||
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']
|
||||
search_fields = ['keyword']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['delete_selected'] # Enable bulk delete
|
||||
actions = [
|
||||
'delete_selected',
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_update_country',
|
||||
] # Enable bulk delete
|
||||
|
||||
fieldsets = (
|
||||
('Keyword Info', {
|
||||
@@ -569,6 +916,50 @@ class SeedKeywordAdmin(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)
|
||||
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)
|
||||
bulk_deactivate.short_description = 'Deactivate selected keywords'
|
||||
|
||||
def bulk_update_country(self, request, queryset):
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
country = request.POST.get('country')
|
||||
if country:
|
||||
updated = queryset.update(country=country)
|
||||
self.message_user(request, f'{updated} seed keyword(s) country updated to: {country}', messages.SUCCESS)
|
||||
return
|
||||
|
||||
COUNTRY_CHOICES = [
|
||||
('US', 'United States'),
|
||||
('GB', 'United Kingdom'),
|
||||
('CA', 'Canada'),
|
||||
('AU', 'Australia'),
|
||||
('IN', 'India'),
|
||||
]
|
||||
|
||||
class CountryForm(forms.Form):
|
||||
country = forms.ChoiceField(
|
||||
choices=COUNTRY_CHOICES,
|
||||
label="Select Country",
|
||||
help_text=f"Update country for {queryset.count()} seed keyword(s)"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Update Country',
|
||||
'queryset': queryset,
|
||||
'form': CountryForm(),
|
||||
'action': 'bulk_update_country',
|
||||
})
|
||||
bulk_update_country.short_description = 'Update country'
|
||||
|
||||
|
||||
class UserResource(resources.ModelResource):
|
||||
@@ -600,6 +991,15 @@ class UserAdmin(ExportMixin, BaseUserAdmin, Igny8ModelAdmin):
|
||||
add_fieldsets = BaseUserAdmin.add_fieldsets + (
|
||||
('IGNY8 Info', {'fields': ('account', 'role')}),
|
||||
)
|
||||
actions = [
|
||||
'bulk_set_role_owner',
|
||||
'bulk_set_role_admin',
|
||||
'bulk_set_role_editor',
|
||||
'bulk_set_role_viewer',
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_send_password_reset',
|
||||
]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Filter users by account for non-superusers"""
|
||||
@@ -619,4 +1019,44 @@ class UserAdmin(ExportMixin, BaseUserAdmin, Igny8ModelAdmin):
|
||||
except:
|
||||
return '-'
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
def bulk_set_role_owner(self, request, queryset):
|
||||
updated = queryset.update(role='owner')
|
||||
self.message_user(request, f'{updated} user(s) role set to Owner.', messages.SUCCESS)
|
||||
bulk_set_role_owner.short_description = 'Set role to Owner'
|
||||
|
||||
def bulk_set_role_admin(self, request, queryset):
|
||||
updated = queryset.update(role='admin')
|
||||
self.message_user(request, f'{updated} user(s) role set to Admin.', messages.SUCCESS)
|
||||
bulk_set_role_admin.short_description = 'Set role to Admin'
|
||||
|
||||
def bulk_set_role_editor(self, request, queryset):
|
||||
updated = queryset.update(role='editor')
|
||||
self.message_user(request, f'{updated} user(s) role set to Editor.', messages.SUCCESS)
|
||||
bulk_set_role_editor.short_description = 'Set role to Editor'
|
||||
|
||||
def bulk_set_role_viewer(self, request, queryset):
|
||||
updated = queryset.update(role='viewer')
|
||||
self.message_user(request, f'{updated} user(s) role set to Viewer.', messages.SUCCESS)
|
||||
bulk_set_role_viewer.short_description = 'Set role to Viewer'
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} user(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate users'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} user(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate users'
|
||||
|
||||
def bulk_send_password_reset(self, request, queryset):
|
||||
# TODO: Implement password reset email sending
|
||||
count = queryset.count()
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} password reset email(s) queued for sending. (Email integration required)',
|
||||
messages.INFO
|
||||
)
|
||||
bulk_send_password_reset.short_description = 'Send password reset email'
|
||||
|
||||
|
||||
@@ -8,12 +8,31 @@ from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import AutomationConfig, AutomationRun
|
||||
|
||||
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class AutomationConfigResource(resources.ModelResource):
|
||||
"""Resource class for exporting Automation Configs"""
|
||||
class Meta:
|
||||
model = AutomationConfig
|
||||
fields = ('id', 'site__domain', 'is_enabled', 'frequency', 'scheduled_time',
|
||||
'within_stage_delay', 'between_stage_delay', 'last_run_at', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(AutomationConfig)
|
||||
class AutomationConfigAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AutomationConfigResource
|
||||
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'within_stage_delay', 'between_stage_delay', 'last_run_at')
|
||||
list_filter = ('is_enabled', 'frequency')
|
||||
search_fields = ('site__domain',)
|
||||
actions = ['bulk_enable', 'bulk_disable']
|
||||
actions = [
|
||||
'bulk_enable',
|
||||
'bulk_disable',
|
||||
'bulk_update_frequency',
|
||||
'bulk_update_delays',
|
||||
]
|
||||
|
||||
def bulk_enable(self, request, queryset):
|
||||
"""Enable selected automation configs"""
|
||||
@@ -26,10 +45,122 @@ class AutomationConfigAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
updated = queryset.update(is_enabled=False)
|
||||
self.message_user(request, f'{updated} automation config(s) disabled.', messages.SUCCESS)
|
||||
bulk_disable.short_description = 'Disable selected automations'
|
||||
|
||||
def bulk_update_frequency(self, request, queryset):
|
||||
"""Update frequency for selected automation configs"""
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
frequency = request.POST.get('frequency')
|
||||
if frequency:
|
||||
updated = queryset.update(frequency=frequency)
|
||||
self.message_user(request, f'{updated} automation config(s) updated to frequency: {frequency}', messages.SUCCESS)
|
||||
return
|
||||
|
||||
FREQUENCY_CHOICES = [
|
||||
('hourly', 'Hourly'),
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
]
|
||||
|
||||
class FrequencyForm(forms.Form):
|
||||
frequency = forms.ChoiceField(
|
||||
choices=FREQUENCY_CHOICES,
|
||||
label="Select Frequency",
|
||||
help_text=f"Update frequency for {queryset.count()} automation config(s)"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Update Automation Frequency',
|
||||
'queryset': queryset,
|
||||
'form': FrequencyForm(),
|
||||
'action': 'bulk_update_frequency',
|
||||
})
|
||||
bulk_update_frequency.short_description = 'Update frequency'
|
||||
|
||||
def bulk_update_delays(self, request, queryset):
|
||||
"""Update delay settings for selected automation configs"""
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
within_delay = int(request.POST.get('within_stage_delay', 0))
|
||||
between_delay = int(request.POST.get('between_stage_delay', 0))
|
||||
|
||||
updated = queryset.update(
|
||||
within_stage_delay=within_delay,
|
||||
between_stage_delay=between_delay
|
||||
)
|
||||
self.message_user(request, f'{updated} automation config(s) delay settings updated.', messages.SUCCESS)
|
||||
return
|
||||
|
||||
class DelayForm(forms.Form):
|
||||
within_stage_delay = forms.IntegerField(
|
||||
min_value=0,
|
||||
initial=10,
|
||||
label="Within Stage Delay (minutes)",
|
||||
help_text="Delay between operations within the same stage"
|
||||
)
|
||||
between_stage_delay = forms.IntegerField(
|
||||
min_value=0,
|
||||
initial=60,
|
||||
label="Between Stage Delay (minutes)",
|
||||
help_text="Delay between different stages"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Update Automation Delays',
|
||||
'queryset': queryset,
|
||||
'form': DelayForm(),
|
||||
'action': 'bulk_update_delays',
|
||||
})
|
||||
bulk_update_delays.short_description = 'Update delay settings'
|
||||
|
||||
|
||||
class AutomationRunResource(resources.ModelResource):
|
||||
"""Resource class for exporting Automation Runs"""
|
||||
class Meta:
|
||||
model = AutomationRun
|
||||
fields = ('id', 'run_id', 'site__domain', 'status', 'current_stage',
|
||||
'started_at', 'completed_at', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(AutomationRun)
|
||||
class AutomationRunAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class AutomationRunAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AutomationRunResource
|
||||
list_display = ('run_id', 'site', 'status', 'current_stage', 'started_at', 'completed_at')
|
||||
list_filter = ('status', 'current_stage')
|
||||
search_fields = ('run_id', 'site__domain')
|
||||
actions = [
|
||||
'bulk_retry_failed',
|
||||
'bulk_cancel_running',
|
||||
'bulk_delete_old_runs',
|
||||
]
|
||||
|
||||
def bulk_retry_failed(self, request, queryset):
|
||||
"""Retry failed automation runs"""
|
||||
failed_runs = queryset.filter(status='failed')
|
||||
count = failed_runs.update(status='pending', current_stage='keyword_research')
|
||||
self.message_user(request, f'{count} failed run(s) marked for retry.', messages.SUCCESS)
|
||||
bulk_retry_failed.short_description = 'Retry failed runs'
|
||||
|
||||
def bulk_cancel_running(self, request, queryset):
|
||||
"""Cancel running automation runs"""
|
||||
running = queryset.filter(status__in=['pending', 'running'])
|
||||
count = running.update(status='failed')
|
||||
self.message_user(request, f'{count} running automation(s) cancelled.', messages.SUCCESS)
|
||||
bulk_cancel_running.short_description = 'Cancel running automations'
|
||||
|
||||
def bulk_delete_old_runs(self, request, queryset):
|
||||
"""Delete automation runs older than 30 days"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = timezone.now() - timedelta(days=30)
|
||||
old_runs = queryset.filter(created_at__lt=cutoff_date)
|
||||
count = old_runs.count()
|
||||
old_runs.delete()
|
||||
self.message_user(request, f'{count} old automation run(s) deleted (older than 30 days).', messages.SUCCESS)
|
||||
bulk_delete_old_runs.short_description = 'Delete old runs (>30 days)'
|
||||
@@ -5,6 +5,7 @@ NOTE: Most billing models are registered in modules/billing/admin.py
|
||||
with full workflow functionality. This file contains legacy/minimal registrations.
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from django.utils.html import format_html
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
@@ -49,8 +50,22 @@ from .models import (
|
||||
# PaymentMethodConfig and AccountPaymentMethod are kept here as they're not duplicated
|
||||
# or have minimal implementations that don't conflict
|
||||
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class AccountPaymentMethodResource(resources.ModelResource):
|
||||
"""Resource class for exporting Account Payment Methods"""
|
||||
class Meta:
|
||||
model = AccountPaymentMethod
|
||||
fields = ('id', 'display_name', 'type', 'account__name', 'is_default',
|
||||
'is_enabled', 'is_verified', 'country_code', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(AccountPaymentMethod)
|
||||
class AccountPaymentMethodAdmin(Igny8ModelAdmin):
|
||||
class AccountPaymentMethodAdmin(ExportMixin, Igny8ModelAdmin):
|
||||
resource_class = AccountPaymentMethodResource
|
||||
list_display = [
|
||||
'display_name',
|
||||
'type',
|
||||
@@ -64,6 +79,12 @@ class AccountPaymentMethodAdmin(Igny8ModelAdmin):
|
||||
list_filter = ['type', 'is_default', 'is_enabled', 'is_verified', 'country_code']
|
||||
search_fields = ['display_name', 'account__name', 'account__id']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_enable',
|
||||
'bulk_disable',
|
||||
'bulk_set_default',
|
||||
'bulk_delete_methods',
|
||||
]
|
||||
fieldsets = (
|
||||
('Payment Method', {
|
||||
'fields': ('account', 'type', 'display_name', 'is_default', 'is_enabled', 'is_verified', 'country_code')
|
||||
@@ -75,4 +96,48 @@ class AccountPaymentMethodAdmin(Igny8ModelAdmin):
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
)
|
||||
def bulk_enable(self, request, queryset):
|
||||
updated = queryset.update(is_enabled=True)
|
||||
self.message_user(request, f'{updated} payment method(s) enabled.', messages.SUCCESS)
|
||||
bulk_enable.short_description = 'Enable selected payment methods'
|
||||
|
||||
def bulk_disable(self, request, queryset):
|
||||
updated = queryset.update(is_enabled=False)
|
||||
self.message_user(request, f'{updated} payment method(s) disabled.', messages.SUCCESS)
|
||||
bulk_disable.short_description = 'Disable selected payment methods'
|
||||
|
||||
def bulk_set_default(self, request, queryset):
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
method_id = request.POST.get('payment_method')
|
||||
if method_id:
|
||||
method = AccountPaymentMethod.objects.get(pk=method_id)
|
||||
# Unset all others for this account
|
||||
AccountPaymentMethod.objects.filter(account=method.account).update(is_default=False)
|
||||
method.is_default = True
|
||||
method.save()
|
||||
self.message_user(request, f'{method.display_name} set as default for {method.account.name}.', messages.SUCCESS)
|
||||
return
|
||||
|
||||
class PaymentMethodForm(forms.Form):
|
||||
payment_method = forms.ModelChoiceField(
|
||||
queryset=queryset,
|
||||
label="Select Payment Method to Set as Default"
|
||||
)
|
||||
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Set Default Payment Method',
|
||||
'queryset': queryset,
|
||||
'form': PaymentMethodForm(),
|
||||
'action': 'bulk_set_default',
|
||||
})
|
||||
bulk_set_default.short_description = 'Set as default'
|
||||
|
||||
def bulk_delete_methods(self, request, queryset):
|
||||
count = queryset.count()
|
||||
queryset.delete()
|
||||
self.message_user(request, f'{count} payment method(s) deleted.', messages.SUCCESS)
|
||||
bulk_delete_methods.short_description = 'Delete selected payment methods'
|
||||
@@ -16,8 +16,18 @@ class SyncEventResource(resources.ModelResource):
|
||||
export_order = fields
|
||||
|
||||
|
||||
class SiteIntegrationResource(resources.ModelResource):
|
||||
"""Resource class for exporting Site Integrations"""
|
||||
class Meta:
|
||||
model = SiteIntegration
|
||||
fields = ('id', 'site__name', 'platform', 'platform_type', 'is_active',
|
||||
'sync_enabled', 'sync_status', 'last_sync_at', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(SiteIntegration)
|
||||
class SiteIntegrationAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class SiteIntegrationAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = SiteIntegrationResource
|
||||
list_display = [
|
||||
'site',
|
||||
'platform',
|
||||
@@ -30,7 +40,13 @@ class SiteIntegrationAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['platform', 'platform_type', 'is_active', 'sync_enabled', 'sync_status']
|
||||
search_fields = ['site__name', 'site__domain', 'platform']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['bulk_enable_sync', 'bulk_disable_sync', 'bulk_trigger_sync']
|
||||
actions = [
|
||||
'bulk_enable_sync',
|
||||
'bulk_disable_sync',
|
||||
'bulk_trigger_sync',
|
||||
'bulk_test_connection',
|
||||
'bulk_delete_integrations',
|
||||
]
|
||||
|
||||
def bulk_enable_sync(self, request, queryset):
|
||||
"""Enable sync for selected integrations"""
|
||||
@@ -52,6 +68,29 @@ class SiteIntegrationAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
count += 1
|
||||
self.message_user(request, f'{count} integration(s) queued for sync.', messages.INFO)
|
||||
bulk_trigger_sync.short_description = 'Trigger sync now'
|
||||
|
||||
def bulk_test_connection(self, request, queryset):
|
||||
"""Test connection for selected integrations"""
|
||||
tested = 0
|
||||
successful = 0
|
||||
for integration in queryset.filter(is_active=True):
|
||||
# TODO: Implement actual connection test logic
|
||||
tested += 1
|
||||
successful += 1 # Placeholder
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'Tested {tested} integration(s). {successful} successful. (Connection test logic to be implemented)',
|
||||
messages.INFO
|
||||
)
|
||||
bulk_test_connection.short_description = 'Test connections'
|
||||
|
||||
def bulk_delete_integrations(self, request, queryset):
|
||||
"""Delete selected integrations"""
|
||||
count = queryset.count()
|
||||
queryset.delete()
|
||||
self.message_user(request, f'{count} integration(s) deleted.', messages.SUCCESS)
|
||||
bulk_delete_integrations.short_description = 'Delete selected integrations'
|
||||
|
||||
|
||||
@admin.register(SyncEvent)
|
||||
@@ -69,7 +108,10 @@ class SyncEventAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['event_type', 'action', 'success', 'created_at']
|
||||
search_fields = ['integration__site__name', 'site__name', 'description', 'external_id']
|
||||
readonly_fields = ['created_at']
|
||||
actions = ['bulk_mark_reviewed']
|
||||
actions = [
|
||||
'bulk_mark_reviewed',
|
||||
'bulk_delete_old_events',
|
||||
]
|
||||
|
||||
def bulk_mark_reviewed(self, request, queryset):
|
||||
"""Mark selected sync events as reviewed"""
|
||||
@@ -77,4 +119,16 @@ class SyncEventAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
count = queryset.count()
|
||||
self.message_user(request, f'{count} sync event(s) marked as reviewed.', messages.SUCCESS)
|
||||
bulk_mark_reviewed.short_description = 'Mark as reviewed'
|
||||
|
||||
def bulk_delete_old_events(self, request, queryset):
|
||||
"""Delete sync events older than 30 days"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = timezone.now() - timedelta(days=30)
|
||||
old_events = queryset.filter(created_at__lt=cutoff_date)
|
||||
count = old_events.count()
|
||||
old_events.delete()
|
||||
self.message_user(request, f'{count} old sync event(s) deleted (older than 30 days).', messages.SUCCESS)
|
||||
bulk_delete_old_events.short_description = 'Delete old events (>30 days)'
|
||||
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import OptimizationTask
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class OptimizationTaskResource(resources.ModelResource):
|
||||
"""Resource class for exporting Optimization Tasks"""
|
||||
class Meta:
|
||||
model = OptimizationTask
|
||||
fields = ('id', 'content__title', 'account__name', 'status',
|
||||
'credits_used', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(OptimizationTask)
|
||||
class OptimizationTaskAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class OptimizationTaskAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = OptimizationTaskResource
|
||||
list_display = ['content', 'account', 'status', 'credits_used', 'created_at']
|
||||
list_filter = ['status', 'created_at']
|
||||
search_fields = ['content__title', 'account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
actions = [
|
||||
'bulk_mark_completed',
|
||||
'bulk_mark_failed',
|
||||
'bulk_retry',
|
||||
]
|
||||
|
||||
def bulk_mark_completed(self, request, queryset):
|
||||
updated = queryset.update(status='completed')
|
||||
self.message_user(request, f'{updated} optimization task(s) marked as completed.', messages.SUCCESS)
|
||||
bulk_mark_completed.short_description = 'Mark as completed'
|
||||
|
||||
def bulk_mark_failed(self, request, queryset):
|
||||
updated = queryset.update(status='failed')
|
||||
self.message_user(request, f'{updated} optimization task(s) marked as failed.', messages.SUCCESS)
|
||||
bulk_mark_failed.short_description = 'Mark as failed'
|
||||
|
||||
def bulk_retry(self, request, queryset):
|
||||
updated = queryset.filter(status='failed').update(status='pending')
|
||||
self.message_user(request, f'{updated} failed optimization task(s) queued for retry.', messages.SUCCESS)
|
||||
bulk_retry.short_description = 'Retry failed tasks'
|
||||
|
||||
@@ -31,7 +31,11 @@ class PublishingRecordAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['destination', 'status', 'site']
|
||||
search_fields = ['content__title', 'destination', 'destination_url']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = ['bulk_retry_failed']
|
||||
actions = [
|
||||
'bulk_retry_failed',
|
||||
'bulk_cancel_pending',
|
||||
'bulk_mark_published',
|
||||
]
|
||||
|
||||
def bulk_retry_failed(self, request, queryset):
|
||||
"""Retry failed publishing records"""
|
||||
@@ -39,10 +43,34 @@ class PublishingRecordAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
count = failed_records.update(status='pending')
|
||||
self.message_user(request, f'{count} failed record(s) marked for retry.', messages.SUCCESS)
|
||||
bulk_retry_failed.short_description = 'Retry failed publishes'
|
||||
|
||||
def bulk_cancel_pending(self, request, queryset):
|
||||
"""Cancel pending publishing records"""
|
||||
pending = queryset.filter(status__in=['pending', 'publishing'])
|
||||
count = pending.update(status='failed', error_message='Cancelled by admin')
|
||||
self.message_user(request, f'{count} publishing record(s) cancelled.', messages.SUCCESS)
|
||||
bulk_cancel_pending.short_description = 'Cancel pending publishes'
|
||||
|
||||
def bulk_mark_published(self, request, queryset):
|
||||
"""Mark selected records as published"""
|
||||
from django.utils import timezone
|
||||
count = queryset.update(status='published', published_at=timezone.now())
|
||||
self.message_user(request, f'{count} record(s) marked as published.', messages.SUCCESS)
|
||||
bulk_mark_published.short_description = 'Mark as published'
|
||||
|
||||
|
||||
class DeploymentRecordResource(resources.ModelResource):
|
||||
"""Resource class for exporting Deployment Records"""
|
||||
class Meta:
|
||||
model = DeploymentRecord
|
||||
fields = ('id', 'site__name', 'sector__name', 'version', 'deployed_version',
|
||||
'status', 'deployment_url', 'deployed_at', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(DeploymentRecord)
|
||||
class DeploymentRecordAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
class DeploymentRecordAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = DeploymentRecordResource
|
||||
list_display = [
|
||||
'site',
|
||||
'sector',
|
||||
@@ -55,4 +83,35 @@ class DeploymentRecordAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['status', 'site']
|
||||
search_fields = ['site__name', 'deployment_url']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_retry_failed',
|
||||
'bulk_rollback',
|
||||
'bulk_cancel_pending',
|
||||
]
|
||||
actions = [
|
||||
'bulk_retry_failed',
|
||||
'bulk_rollback',
|
||||
'bulk_cancel_pending',
|
||||
]
|
||||
|
||||
def bulk_retry_failed(self, request, queryset):
|
||||
"""Retry failed deployments"""
|
||||
failed = queryset.filter(status='failed')
|
||||
count = failed.update(status='pending', error_message='')
|
||||
self.message_user(request, f'{count} failed deployment(s) marked for retry.', messages.SUCCESS)
|
||||
bulk_retry_failed.short_description = 'Retry failed deployments'
|
||||
|
||||
def bulk_rollback(self, request, queryset):
|
||||
"""Rollback selected deployments"""
|
||||
deployed = queryset.filter(status='deployed')
|
||||
count = deployed.update(status='rolled_back')
|
||||
self.message_user(request, f'{count} deployment(s) marked for rollback.', messages.SUCCESS)
|
||||
bulk_rollback.short_description = 'Rollback deployments'
|
||||
|
||||
def bulk_cancel_pending(self, request, queryset):
|
||||
"""Cancel pending deployments"""
|
||||
pending = queryset.filter(status__in=['pending', 'deploying'])
|
||||
count = pending.update(status='failed', error_message='Cancelled by admin')
|
||||
self.message_user(request, f'{count} deployment(s) cancelled.', messages.SUCCESS)
|
||||
bulk_cancel_pending.short_description = 'Cancel pending deployments'
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from igny8_core.business.billing.models import (
|
||||
PlanLimitUsage,
|
||||
)
|
||||
from .models import CreditTransaction, CreditUsageLog, AccountPaymentMethod
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
from rangefilter.filters import DateRangeFilter
|
||||
|
||||
@@ -50,8 +50,18 @@ class CreditTransactionAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
|
||||
class CreditUsageLogResource(resources.ModelResource):
|
||||
"""Resource class for exporting Credit Usage Logs"""
|
||||
class Meta:
|
||||
model = CreditUsageLog
|
||||
fields = ('id', 'account__name', 'operation_type', 'credits_used', 'cost_usd',
|
||||
'model_used', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(CreditUsageLog)
|
||||
class CreditUsageLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class CreditUsageLogAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = CreditUsageLogResource
|
||||
list_display = ['id', 'account', 'operation_type', 'credits_used', 'cost_usd', 'model_used', 'created_at']
|
||||
list_filter = ['operation_type', 'created_at', 'account', 'model_used']
|
||||
search_fields = ['account__name', 'model_used']
|
||||
@@ -68,8 +78,18 @@ class CreditUsageLogAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
|
||||
class InvoiceResource(resources.ModelResource):
|
||||
"""Resource class for exporting Invoices"""
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ('id', 'invoice_number', 'account__name', 'status', 'total', 'currency',
|
||||
'invoice_date', 'due_date', 'created_at', 'updated_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
class InvoiceAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class InvoiceAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = InvoiceResource
|
||||
list_display = [
|
||||
'invoice_number',
|
||||
'account',
|
||||
@@ -82,6 +102,56 @@ class InvoiceAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
list_filter = ['status', 'currency', 'invoice_date', 'account']
|
||||
search_fields = ['invoice_number', 'account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_set_status_draft',
|
||||
'bulk_set_status_sent',
|
||||
'bulk_set_status_paid',
|
||||
'bulk_set_status_overdue',
|
||||
'bulk_set_status_cancelled',
|
||||
'bulk_send_reminders',
|
||||
]
|
||||
|
||||
def bulk_set_status_draft(self, request, queryset):
|
||||
"""Set selected invoices to draft status"""
|
||||
updated = queryset.update(status='draft')
|
||||
self.message_user(request, f'{updated} invoice(s) set to draft.', messages.SUCCESS)
|
||||
bulk_set_status_draft.short_description = 'Set status to Draft'
|
||||
|
||||
def bulk_set_status_sent(self, request, queryset):
|
||||
"""Set selected invoices to sent status"""
|
||||
updated = queryset.update(status='sent')
|
||||
self.message_user(request, f'{updated} invoice(s) set to sent.', messages.SUCCESS)
|
||||
bulk_set_status_sent.short_description = 'Set status to Sent'
|
||||
|
||||
def bulk_set_status_paid(self, request, queryset):
|
||||
"""Set selected invoices to paid status"""
|
||||
updated = queryset.update(status='paid')
|
||||
self.message_user(request, f'{updated} invoice(s) set to paid.', messages.SUCCESS)
|
||||
bulk_set_status_paid.short_description = 'Set status to Paid'
|
||||
|
||||
def bulk_set_status_overdue(self, request, queryset):
|
||||
"""Set selected invoices to overdue status"""
|
||||
updated = queryset.update(status='overdue')
|
||||
self.message_user(request, f'{updated} invoice(s) set to overdue.', messages.SUCCESS)
|
||||
bulk_set_status_overdue.short_description = 'Set status to Overdue'
|
||||
|
||||
def bulk_set_status_cancelled(self, request, queryset):
|
||||
"""Set selected invoices to cancelled status"""
|
||||
updated = queryset.update(status='cancelled')
|
||||
self.message_user(request, f'{updated} invoice(s) set to cancelled.', messages.SUCCESS)
|
||||
bulk_set_status_cancelled.short_description = 'Set status to Cancelled'
|
||||
|
||||
def bulk_send_reminders(self, request, queryset):
|
||||
"""Send reminder emails for selected invoices"""
|
||||
# TODO: Implement email sending logic when email service is configured
|
||||
unpaid = queryset.filter(status__in=['sent', 'overdue'])
|
||||
count = unpaid.count()
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} invoice reminder(s) queued for sending. (Email integration required)',
|
||||
messages.INFO
|
||||
)
|
||||
bulk_send_reminders.short_description = 'Send payment reminders'
|
||||
|
||||
|
||||
class PaymentResource(resources.ModelResource):
|
||||
@@ -128,7 +198,7 @@ class PaymentAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
'manual_notes'
|
||||
]
|
||||
readonly_fields = ['created_at', 'updated_at', 'approved_at', 'processed_at', 'failed_at', 'refunded_at']
|
||||
actions = ['approve_payments', 'reject_payments']
|
||||
actions = ['approve_payments', 'reject_payments', 'bulk_refund']
|
||||
|
||||
fieldsets = (
|
||||
('Payment Info', {
|
||||
@@ -374,14 +444,71 @@ class PaymentAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
|
||||
self.message_user(request, f'Rejected {count} payment(s)')
|
||||
|
||||
reject_payments.short_description = 'Reject selected manual payments'
|
||||
|
||||
def bulk_refund(self, request, queryset):
|
||||
"""Refund selected payments"""
|
||||
from django.utils import timezone
|
||||
|
||||
# Only refund succeeded payments
|
||||
succeeded_payments = queryset.filter(status='succeeded')
|
||||
count = 0
|
||||
|
||||
for payment in succeeded_payments:
|
||||
# Mark as refunded
|
||||
payment.status = 'refunded'
|
||||
payment.refunded_at = timezone.now()
|
||||
payment.admin_notes = f'{payment.admin_notes or ""}\nBulk refunded by {request.user.email} on {timezone.now()}'
|
||||
payment.save()
|
||||
|
||||
# TODO: Process actual refund through payment gateway (Stripe/PayPal)
|
||||
# For now, just marking as refunded in database
|
||||
|
||||
count += 1
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} payment(s) marked as refunded. Note: Actual gateway refunds need to be processed separately.',
|
||||
messages.WARNING
|
||||
)
|
||||
bulk_refund.short_description = 'Refund selected payments'
|
||||
|
||||
|
||||
class CreditPackageResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Credit Packages"""
|
||||
class Meta:
|
||||
model = CreditPackage
|
||||
fields = ('id', 'name', 'slug', 'credits', 'price', 'discount_percentage',
|
||||
'is_active', 'is_featured', 'sort_order', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(CreditPackage)
|
||||
class CreditPackageAdmin(Igny8ModelAdmin):
|
||||
class CreditPackageAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
resource_class = CreditPackageResource
|
||||
list_display = ['name', 'slug', 'credits', 'price', 'discount_percentage', 'is_active', 'is_featured', 'sort_order']
|
||||
list_filter = ['is_active', 'is_featured']
|
||||
search_fields = ['name', 'slug']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
]
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
]
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} credit package(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected packages'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} credit package(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected packages'
|
||||
|
||||
|
||||
@admin.register(PaymentMethodConfig)
|
||||
@@ -499,8 +626,18 @@ class CreditCostConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
class PlanLimitUsageResource(resources.ModelResource):
|
||||
"""Resource class for exporting Plan Limit Usage"""
|
||||
class Meta:
|
||||
model = PlanLimitUsage
|
||||
fields = ('id', 'account__name', 'limit_type', 'amount_used',
|
||||
'period_start', 'period_end', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(PlanLimitUsage)
|
||||
class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class PlanLimitUsageAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = PlanLimitUsageResource
|
||||
"""Admin for tracking plan limit usage across billing periods"""
|
||||
list_display = [
|
||||
'account',
|
||||
@@ -518,6 +655,10 @@ class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
search_fields = ['account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
date_hierarchy = 'period_start'
|
||||
actions = [
|
||||
'bulk_reset_usage',
|
||||
'bulk_delete_old_records',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Usage Info', {
|
||||
@@ -540,6 +681,24 @@ class PlanLimitUsageAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
"""Display billing period range"""
|
||||
return f"{obj.period_start} to {obj.period_end}"
|
||||
period_display.short_description = 'Billing Period'
|
||||
|
||||
def bulk_reset_usage(self, request, queryset):
|
||||
"""Reset usage counters to zero"""
|
||||
updated = queryset.update(amount_used=0)
|
||||
self.message_user(request, f'{updated} usage counter(s) reset to zero.', messages.SUCCESS)
|
||||
bulk_reset_usage.short_description = 'Reset usage counters'
|
||||
|
||||
def bulk_delete_old_records(self, request, queryset):
|
||||
"""Delete usage records older than 1 year"""
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
cutoff_date = timezone.now() - timedelta(days=365)
|
||||
old_records = queryset.filter(period_end__lt=cutoff_date)
|
||||
count = old_records.count()
|
||||
old_records.delete()
|
||||
self.message_user(request, f'{count} old usage record(s) deleted (older than 1 year).', messages.SUCCESS)
|
||||
bulk_delete_old_records.short_description = 'Delete old records (>1 year)'
|
||||
|
||||
|
||||
@admin.register(BillingConfiguration)
|
||||
|
||||
@@ -9,21 +9,35 @@ from unfold.contrib.filters.admin import (
|
||||
)
|
||||
from igny8_core.admin.base import SiteSectorAdminMixin, Igny8ModelAdmin
|
||||
from .models import Keywords, Clusters, ContentIdeas
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class KeywordsResource(resources.ModelResource):
|
||||
"""Resource class for exporting Keywords"""
|
||||
"""Resource class for importing/exporting Keywords"""
|
||||
class Meta:
|
||||
model = Keywords
|
||||
fields = ('id', 'keyword', 'seed_keyword__keyword', 'site__name', 'sector__name',
|
||||
'cluster__name', 'volume', 'difficulty', 'country', 'status', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
class ClustersResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Clusters"""
|
||||
class Meta:
|
||||
model = Clusters
|
||||
fields = ('id', 'name', 'site__name', 'sector__name', 'keywords_count', 'volume',
|
||||
'status', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(Clusters)
|
||||
class ClustersAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
class ClustersAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = ClustersResource
|
||||
list_display = ['name', 'site', 'sector', 'keywords_count', 'volume', 'status', 'created_at']
|
||||
list_filter = [
|
||||
('status', ChoicesDropdownFilter),
|
||||
@@ -35,6 +49,11 @@ class ClustersAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
search_fields = ['name']
|
||||
ordering = ['name']
|
||||
autocomplete_fields = ['site', 'sector']
|
||||
actions = [
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_inactive',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
def get_site_display(self, obj):
|
||||
"""Safely get site name"""
|
||||
@@ -50,10 +69,31 @@ class ClustersAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
return obj.sector.name if obj.sector else '-'
|
||||
except:
|
||||
return '-'
|
||||
|
||||
def bulk_set_status_active(self, request, queryset):
|
||||
"""Set selected clusters to active status"""
|
||||
updated = queryset.update(status='active')
|
||||
self.message_user(request, f'{updated} cluster(s) set to active.', messages.SUCCESS)
|
||||
bulk_set_status_active.short_description = 'Set status to Active'
|
||||
|
||||
def bulk_set_status_inactive(self, request, queryset):
|
||||
"""Set selected clusters to inactive status"""
|
||||
updated = queryset.update(status='inactive')
|
||||
self.message_user(request, f'{updated} cluster(s) set to inactive.', messages.SUCCESS)
|
||||
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
||||
|
||||
def bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected clusters"""
|
||||
count = 0
|
||||
for cluster in queryset:
|
||||
cluster.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} cluster(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected clusters'
|
||||
|
||||
|
||||
@admin.register(Keywords)
|
||||
class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
class KeywordsAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = KeywordsResource
|
||||
list_display = ['keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'volume', 'difficulty', 'country', 'status', 'created_at']
|
||||
list_editable = ['status'] # Enable inline editing for status
|
||||
@@ -74,6 +114,7 @@ class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
'bulk_assign_cluster',
|
||||
'bulk_set_status_active',
|
||||
'bulk_set_status_inactive',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
def get_site_display(self, obj):
|
||||
@@ -150,10 +191,32 @@ class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
updated = queryset.update(status='inactive')
|
||||
self.message_user(request, f'{updated} keyword(s) set to inactive.', messages.SUCCESS)
|
||||
bulk_set_status_inactive.short_description = 'Set status to Inactive'
|
||||
|
||||
def bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected keywords"""
|
||||
count = 0
|
||||
for keyword in queryset:
|
||||
keyword.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} keyword(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected keywords'
|
||||
|
||||
|
||||
class ContentIdeasResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Content Ideas"""
|
||||
class Meta:
|
||||
model = ContentIdeas
|
||||
fields = ('id', 'idea_title', 'description', 'site__name', 'sector__name',
|
||||
'content_type', 'content_structure', 'status', 'keyword_cluster__name',
|
||||
'target_keywords', 'estimated_word_count', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(ContentIdeas)
|
||||
class ContentIdeasAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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_filter = [
|
||||
('status', ChoicesDropdownFilter),
|
||||
@@ -168,6 +231,15 @@ class ContentIdeasAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
search_fields = ['idea_title', 'target_keywords', 'description']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_set_status_draft',
|
||||
'bulk_set_status_approved',
|
||||
'bulk_set_status_rejected',
|
||||
'bulk_set_status_completed',
|
||||
'bulk_assign_cluster',
|
||||
'bulk_update_content_type',
|
||||
'bulk_soft_delete',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -218,4 +290,109 @@ class ContentIdeasAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
except:
|
||||
return '-'
|
||||
get_keyword_cluster_display.short_description = 'Cluster'
|
||||
|
||||
|
||||
def bulk_set_status_draft(self, request, queryset):
|
||||
"""Set selected content ideas to draft status"""
|
||||
updated = queryset.update(status='draft')
|
||||
self.message_user(request, f'{updated} content idea(s) set to draft.', messages.SUCCESS)
|
||||
bulk_set_status_draft.short_description = 'Set status to Draft'
|
||||
|
||||
def bulk_set_status_approved(self, request, queryset):
|
||||
"""Set selected content ideas to approved status"""
|
||||
updated = queryset.update(status='approved')
|
||||
self.message_user(request, f'{updated} content idea(s) set to approved.', messages.SUCCESS)
|
||||
bulk_set_status_approved.short_description = 'Set status to Approved'
|
||||
|
||||
def bulk_set_status_rejected(self, request, queryset):
|
||||
"""Set selected content ideas to rejected status"""
|
||||
updated = queryset.update(status='rejected')
|
||||
self.message_user(request, f'{updated} content idea(s) set to rejected.', messages.SUCCESS)
|
||||
bulk_set_status_rejected.short_description = 'Set status to Rejected'
|
||||
|
||||
def bulk_set_status_completed(self, request, queryset):
|
||||
"""Set selected content ideas to completed status"""
|
||||
updated = queryset.update(status='completed')
|
||||
self.message_user(request, f'{updated} content idea(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 content ideas to a cluster"""
|
||||
from django import forms
|
||||
|
||||
if 'apply' in request.POST:
|
||||
cluster_id = request.POST.get('cluster')
|
||||
if cluster_id:
|
||||
cluster = Clusters.objects.get(pk=cluster_id)
|
||||
updated = queryset.update(keyword_cluster=cluster)
|
||||
self.message_user(request, f'{updated} content idea(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
|
||||
return
|
||||
|
||||
first_idea = queryset.first()
|
||||
if first_idea:
|
||||
clusters = Clusters.objects.filter(site=first_idea.site, sector=first_idea.sector)
|
||||
else:
|
||||
clusters = Clusters.objects.all()
|
||||
|
||||
class ClusterForm(forms.Form):
|
||||
cluster = forms.ModelChoiceField(
|
||||
queryset=clusters,
|
||||
label="Select Cluster",
|
||||
help_text=f"Assign {queryset.count()} selected content idea(s) to:"
|
||||
)
|
||||
|
||||
if clusters.exists():
|
||||
from django.shortcuts import render
|
||||
return render(request, 'admin/bulk_action_form.html', {
|
||||
'title': 'Assign Content Ideas to Cluster',
|
||||
'queryset': queryset,
|
||||
'form': ClusterForm(),
|
||||
'action': 'bulk_assign_cluster',
|
||||
})
|
||||
else:
|
||||
self.message_user(request, 'No clusters available for the selected content ideas.', messages.WARNING)
|
||||
bulk_assign_cluster.short_description = 'Assign to Cluster'
|
||||
|
||||
def bulk_update_content_type(self, request, queryset):
|
||||
"""Update content type for selected content ideas"""
|
||||
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} content idea(s) updated to content type: {content_type}', messages.SUCCESS)
|
||||
return
|
||||
|
||||
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 content idea(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 bulk_soft_delete(self, request, queryset):
|
||||
"""Soft delete selected content ideas"""
|
||||
count = 0
|
||||
for idea in queryset:
|
||||
idea.delete() # Soft delete via SoftDeletableModel
|
||||
count += 1
|
||||
self.message_user(request, f'{count} content idea(s) soft deleted.', messages.SUCCESS)
|
||||
bulk_soft_delete.short_description = 'Soft delete selected content ideas'
|
||||
|
||||
@@ -6,6 +6,21 @@ from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
|
||||
|
||||
from django.contrib import messages
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
class AIPromptResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting AI Prompts"""
|
||||
class Meta:
|
||||
model = AIPrompt
|
||||
fields = ('id', 'account__name', 'prompt_type', 'prompt_value', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
# Import settings admin
|
||||
from .settings_admin import (
|
||||
SystemSettingsAdmin, AccountSettingsAdmin, UserSettingsAdmin,
|
||||
@@ -35,11 +50,17 @@ except ImportError:
|
||||
|
||||
|
||||
@admin.register(AIPrompt)
|
||||
class AIPromptAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AIPromptResource
|
||||
list_display = ['id', 'prompt_type', 'account', 'is_active', 'updated_at']
|
||||
list_filter = ['prompt_type', 'is_active', 'account']
|
||||
search_fields = ['prompt_type']
|
||||
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_reset_to_default',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -61,14 +82,48 @@ class AIPromptAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
except:
|
||||
return '-'
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} AI prompt(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected prompts'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} AI prompt(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected prompts'
|
||||
|
||||
def bulk_reset_to_default(self, request, queryset):
|
||||
count = 0
|
||||
for prompt in queryset:
|
||||
if prompt.default_prompt:
|
||||
prompt.prompt_value = prompt.default_prompt
|
||||
prompt.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} AI prompt(s) reset to default values.', messages.SUCCESS)
|
||||
bulk_reset_to_default.short_description = 'Reset to default values'
|
||||
|
||||
|
||||
class IntegrationSettingsResource(resources.ModelResource):
|
||||
"""Resource class for exporting Integration Settings (config masked)"""
|
||||
class Meta:
|
||||
model = IntegrationSettings
|
||||
fields = ('id', 'account__name', 'integration_type', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
|
||||
|
||||
@admin.register(IntegrationSettings)
|
||||
class IntegrationSettingsAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class IntegrationSettingsAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = IntegrationSettingsResource
|
||||
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
|
||||
list_filter = ['integration_type', 'is_active', 'account']
|
||||
search_fields = ['integration_type']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_test_connection',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -97,14 +152,50 @@ class IntegrationSettingsAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
except:
|
||||
return '-'
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} integration setting(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected integrations'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} integration setting(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected integrations'
|
||||
|
||||
def bulk_test_connection(self, request, queryset):
|
||||
"""Test connection for selected integration settings"""
|
||||
count = queryset.filter(is_active=True).count()
|
||||
self.message_user(
|
||||
request,
|
||||
f'{count} integration(s) queued for connection test. (Test logic to be implemented)',
|
||||
messages.INFO
|
||||
)
|
||||
bulk_test_connection.short_description = 'Test connections'
|
||||
|
||||
|
||||
class AuthorProfileResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Author Profiles"""
|
||||
class Meta:
|
||||
model = AuthorProfile
|
||||
fields = ('id', 'name', 'account__name', 'tone', 'language', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(AuthorProfile)
|
||||
class AuthorProfileAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class AuthorProfileAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AuthorProfileResource
|
||||
list_display = ['name', 'account', 'tone', 'language', 'is_active', 'created_at']
|
||||
list_filter = ['is_active', 'tone', 'language', 'account']
|
||||
search_fields = ['name', 'description', 'tone']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_clone',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -126,14 +217,52 @@ class AuthorProfileAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
except:
|
||||
return '-'
|
||||
get_account_display.short_description = 'Account'
|
||||
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} author profile(s) activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected profiles'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} author profile(s) deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected profiles'
|
||||
|
||||
def bulk_clone(self, request, queryset):
|
||||
count = 0
|
||||
for profile in queryset:
|
||||
profile_copy = AuthorProfile.objects.get(pk=profile.pk)
|
||||
profile_copy.pk = None
|
||||
profile_copy.name = f"{profile.name} (Copy)"
|
||||
profile_copy.is_active = False
|
||||
profile_copy.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} author profile(s) cloned.', messages.SUCCESS)
|
||||
bulk_clone.short_description = 'Clone selected profiles'
|
||||
|
||||
|
||||
class StrategyResource(resources.ModelResource):
|
||||
"""Resource class for importing/exporting Strategies"""
|
||||
class Meta:
|
||||
model = Strategy
|
||||
fields = ('id', 'name', 'account__name', 'sector__name', 'is_active', 'created_at')
|
||||
export_order = fields
|
||||
import_id_fields = ('id',)
|
||||
skip_unchanged = True
|
||||
|
||||
|
||||
@admin.register(Strategy)
|
||||
class StrategyAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = StrategyResource
|
||||
list_display = ['name', 'account', 'sector', 'is_active', 'created_at']
|
||||
list_filter = ['is_active', 'account']
|
||||
search_fields = ['name', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_clone',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
@@ -162,4 +291,25 @@ class StrategyAdmin(AccountAdminMixin, Igny8ModelAdmin):
|
||||
return obj.sector.name if obj.sector else 'Global'
|
||||
except:
|
||||
return 'Global'
|
||||
get_sector_display.short_description = 'Sector'
|
||||
get_sector_display.short_description = 'Sector'
|
||||
def bulk_activate(self, request, queryset):
|
||||
updated = queryset.update(is_active=True)
|
||||
self.message_user(request, f'{updated} strategy/strategies activated.', messages.SUCCESS)
|
||||
bulk_activate.short_description = 'Activate selected strategies'
|
||||
|
||||
def bulk_deactivate(self, request, queryset):
|
||||
updated = queryset.update(is_active=False)
|
||||
self.message_user(request, f'{updated} strategy/strategies deactivated.', messages.SUCCESS)
|
||||
bulk_deactivate.short_description = 'Deactivate selected strategies'
|
||||
|
||||
def bulk_clone(self, request, queryset):
|
||||
count = 0
|
||||
for strategy in queryset:
|
||||
strategy_copy = Strategy.objects.get(pk=strategy.pk)
|
||||
strategy_copy.pk = None
|
||||
strategy_copy.name = f"{strategy.name} (Copy)"
|
||||
strategy_copy.is_active = False
|
||||
strategy_copy.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} strategy/strategies cloned.', messages.SUCCESS)
|
||||
bulk_clone.short_description = 'Clone selected strategies'
|
||||
@@ -10,7 +10,7 @@ from unfold.contrib.filters.admin import (
|
||||
from igny8_core.admin.base import SiteSectorAdminMixin, Igny8ModelAdmin
|
||||
from .models import Tasks, Images, Content
|
||||
from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute, ContentTaxonomyRelation, ContentClusterMap
|
||||
from import_export.admin import ExportMixin
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
from import_export import resources
|
||||
|
||||
|
||||
@@ -24,16 +24,18 @@ class ContentTaxonomyInline(TabularInline):
|
||||
|
||||
|
||||
class TaskResource(resources.ModelResource):
|
||||
"""Resource class for exporting Tasks"""
|
||||
"""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(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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
|
||||
@@ -55,6 +57,8 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
'bulk_set_status_in_progress',
|
||||
'bulk_set_status_completed',
|
||||
'bulk_assign_cluster',
|
||||
'bulk_soft_delete',
|
||||
'bulk_update_content_type',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
@@ -132,6 +136,52 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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:
|
||||
@@ -156,12 +206,29 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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"""
|
||||
@@ -186,20 +253,62 @@ class ImagesAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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'
|
||||
|
||||
|
||||
class ContentResource(resources.ModelResource):
|
||||
"""Resource class for exporting Content"""
|
||||
"""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', 'external_url', 'created_at')
|
||||
'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(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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 = [
|
||||
@@ -222,6 +331,9 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
'bulk_set_status_published',
|
||||
'bulk_set_status_draft',
|
||||
'bulk_add_taxonomy',
|
||||
'bulk_soft_delete',
|
||||
'bulk_publish_to_wordpress',
|
||||
'bulk_unpublish_from_wordpress',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
@@ -335,6 +447,57 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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:
|
||||
@@ -351,13 +514,29 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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', {
|
||||
@@ -380,14 +559,77 @@ class ContentTaxonomyAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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', {
|
||||
@@ -405,19 +647,222 @@ class ContentAttributeAdmin(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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(Igny8ModelAdmin):
|
||||
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(SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user