models delte and trash and cascade issues fixes
This commit is contained in:
@@ -12,8 +12,9 @@ from unfold.admin import ModelAdmin
|
||||
|
||||
# Import all soft-deletable models
|
||||
from igny8_core.auth.models import Account, Site, Sector
|
||||
from igny8_core.business.content.models import Tasks, Content, Images
|
||||
from igny8_core.business.content.models import Tasks, Content, Images, ContentTaxonomy
|
||||
from igny8_core.business.planning.models import Clusters, Keywords, ContentIdeas
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class DeletedOnlyManager:
|
||||
@@ -157,9 +158,17 @@ class BaseTrashAdmin(ModelAdmin):
|
||||
expired = queryset.filter(restore_until__lt=now)
|
||||
|
||||
restored_count = 0
|
||||
failed_count = 0
|
||||
failed_items = []
|
||||
|
||||
for obj in restorable:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
obj.restore()
|
||||
restored_count += 1
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
failed_items.append(f"{obj} ({str(e)[:50]}...)")
|
||||
|
||||
if restored_count > 0:
|
||||
self.message_user(
|
||||
@@ -168,6 +177,13 @@ class BaseTrashAdmin(ModelAdmin):
|
||||
messages.SUCCESS
|
||||
)
|
||||
|
||||
if failed_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
f'{failed_count} record(s) could not be restored (constraint violation). Items: {", ".join(failed_items[:3])}',
|
||||
messages.ERROR
|
||||
)
|
||||
|
||||
if expired.exists():
|
||||
self.message_user(
|
||||
request,
|
||||
@@ -179,15 +195,72 @@ class BaseTrashAdmin(ModelAdmin):
|
||||
def permanently_delete_records(self, request, queryset):
|
||||
"""Permanently delete selected records (irreversible)."""
|
||||
count = queryset.count()
|
||||
for obj in queryset:
|
||||
obj.hard_delete()
|
||||
failed_count = 0
|
||||
failed_items = []
|
||||
deleted_count = 0
|
||||
|
||||
for obj in queryset:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# For Sector, cascade delete related soft-deleted records first
|
||||
if isinstance(obj, Sector):
|
||||
self._cascade_delete_sector_relations(obj)
|
||||
# For Site, cascade delete related soft-deleted records first
|
||||
elif isinstance(obj, Site):
|
||||
self._cascade_delete_site_relations(obj)
|
||||
obj.hard_delete()
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
failed_items.append(f"{obj} ({str(e)[:100]}...)")
|
||||
|
||||
if deleted_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
f'Permanently deleted {count} record(s). This cannot be undone.',
|
||||
f'Permanently deleted {deleted_count} record(s). This cannot be undone.',
|
||||
messages.SUCCESS
|
||||
)
|
||||
|
||||
if failed_count > 0:
|
||||
self.message_user(
|
||||
request,
|
||||
f'{failed_count} record(s) could not be deleted. Items: {", ".join(failed_items[:3])}',
|
||||
messages.ERROR
|
||||
)
|
||||
|
||||
def _cascade_delete_sector_relations(self, sector):
|
||||
"""Cascade delete all related soft-deleted records for a sector."""
|
||||
# Delete in order of dependencies
|
||||
# 1. Images (depends on Content/Tasks)
|
||||
Images.all_objects.filter(sector=sector).delete()
|
||||
# 2. Content (depends on Tasks)
|
||||
Content.all_objects.filter(sector=sector).delete()
|
||||
# 3. Tasks (depends on Clusters)
|
||||
Tasks.all_objects.filter(sector=sector).delete()
|
||||
# 4. ContentIdeas (depends on Clusters)
|
||||
ContentIdeas.all_objects.filter(sector=sector).delete()
|
||||
# 5. Keywords (depends on Clusters)
|
||||
Keywords.all_objects.filter(sector=sector).delete()
|
||||
# 6. Clusters
|
||||
Clusters.all_objects.filter(sector=sector).delete()
|
||||
# 7. ContentTaxonomy (if exists)
|
||||
ContentTaxonomy.objects.filter(sector=sector).delete()
|
||||
|
||||
def _cascade_delete_site_relations(self, site):
|
||||
"""Cascade delete all related soft-deleted records for a site."""
|
||||
# Delete sectors first (which will cascade to their relations)
|
||||
for sector in Sector.all_objects.filter(site=site):
|
||||
self._cascade_delete_sector_relations(sector)
|
||||
sector.hard_delete()
|
||||
# Delete any site-level records
|
||||
Images.all_objects.filter(site=site).delete()
|
||||
Content.all_objects.filter(site=site).delete()
|
||||
Tasks.all_objects.filter(site=site).delete()
|
||||
ContentIdeas.all_objects.filter(site=site).delete()
|
||||
Keywords.all_objects.filter(site=site).delete()
|
||||
Clusters.all_objects.filter(site=site).delete()
|
||||
ContentTaxonomy.objects.filter(site=site).delete()
|
||||
|
||||
actions = ['restore_records', 'permanently_delete_records']
|
||||
|
||||
|
||||
@@ -442,6 +515,31 @@ class ContentIdeasTrashAdmin(BaseTrashAdmin):
|
||||
site_name.short_description = 'Site'
|
||||
|
||||
|
||||
class ContentTaxonomyTrashAdmin(BaseTrashAdmin):
|
||||
"""Trash view for deleted Content Taxonomies (Tags/Categories)."""
|
||||
list_display = [
|
||||
'name', 'taxonomy_type', 'site_name', 'deleted_at_display',
|
||||
'time_until_permanent_delete', 'can_restore_display'
|
||||
]
|
||||
list_filter = [RestorableTimePeriodFilter, DeletedInLastFilter, 'taxonomy_type']
|
||||
search_fields = ['name', 'slug', 'site__name']
|
||||
ordering = ['-deleted_at']
|
||||
readonly_fields = [
|
||||
'name', 'slug', 'taxonomy_type', 'description', 'account', 'site', 'sector',
|
||||
'external_id', 'external_taxonomy', 'count',
|
||||
'is_deleted', 'deleted_at', 'restore_until', 'delete_reason', 'deleted_by'
|
||||
]
|
||||
|
||||
def site_name(self, obj):
|
||||
if obj.site:
|
||||
try:
|
||||
return Site.all_objects.get(pk=obj.site_id).name
|
||||
except Site.DoesNotExist:
|
||||
return f'[Deleted #{obj.site_id}]'
|
||||
return '-'
|
||||
site_name.short_description = 'Site'
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PROXY MODELS FOR TRASH VIEWS
|
||||
# ============================================================================
|
||||
@@ -527,6 +625,15 @@ class ContentIdeasTrash(ContentIdeas):
|
||||
verbose_name_plural = 'Content Ideas (Trash)'
|
||||
|
||||
|
||||
class ContentTaxonomyTrash(ContentTaxonomy):
|
||||
"""Proxy model for Content Taxonomy (Tags/Categories) trash view."""
|
||||
class Meta:
|
||||
proxy = True
|
||||
app_label = 'writer'
|
||||
verbose_name = 'Taxonomy (Trash)'
|
||||
verbose_name_plural = 'Taxonomies (Trash)'
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# REGISTER TRASH ADMINS
|
||||
# ============================================================================
|
||||
@@ -540,3 +647,4 @@ admin.site.register(ImagesTrash, ImagesTrashAdmin)
|
||||
admin.site.register(ClustersTrash, ClustersTrashAdmin)
|
||||
admin.site.register(KeywordsTrash, KeywordsTrashAdmin)
|
||||
admin.site.register(ContentIdeasTrash, ContentIdeasTrashAdmin)
|
||||
admin.site.register(ContentTaxonomyTrash, ContentTaxonomyTrashAdmin)
|
||||
|
||||
@@ -440,10 +440,11 @@ class Content(SoftDeletableModel, SiteSectorBaseModel):
|
||||
return super().hard_delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
|
||||
class ContentTaxonomy(SiteSectorBaseModel):
|
||||
class ContentTaxonomy(SoftDeletableModel, SiteSectorBaseModel):
|
||||
"""
|
||||
Simplified taxonomy model for AI-generated categories and tags.
|
||||
Directly linked to Content via many-to-many relationship.
|
||||
Supports soft-delete for trash/restore functionality.
|
||||
"""
|
||||
|
||||
TAXONOMY_TYPE_CHOICES = [
|
||||
@@ -497,6 +498,9 @@ class ContentTaxonomy(SiteSectorBaseModel):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = SoftDeleteManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class Meta:
|
||||
app_label = 'writer'
|
||||
db_table = 'igny8_content_taxonomy_terms'
|
||||
|
||||
@@ -79,7 +79,7 @@ class Keywords(SoftDeletableModel, SiteSectorBaseModel):
|
||||
# Required: Link to global SeedKeyword
|
||||
seed_keyword = models.ForeignKey(
|
||||
SeedKeyword,
|
||||
on_delete=models.PROTECT, # Prevent deletion if Keywords reference it
|
||||
on_delete=models.CASCADE, # Allow deletion of SeedKeyword (cascades to Keywords)
|
||||
related_name='site_keywords',
|
||||
help_text="Reference to the global seed keyword"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-12 16:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('igny8_core_auth', '0021_accounttrash_sectortrash_sitetrash'),
|
||||
('planner', '0009_clusterstrash_contentideastrash_keywordstrash'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='keywords',
|
||||
name='seed_keyword',
|
||||
field=models.ForeignKey(help_text='Reference to the global seed keyword', on_delete=django.db.models.deletion.CASCADE, related_name='site_keywords', to='igny8_core_auth.seedkeyword'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-12 16:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('writer', '0017_contenttrash_imagestrash_taskstrash'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentTaxonomyTrash',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Taxonomy (Trash)',
|
||||
'verbose_name_plural': 'Taxonomies (Trash)',
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('writer.contenttaxonomy',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contenttaxonomy',
|
||||
name='delete_reason',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contenttaxonomy',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contenttaxonomy',
|
||||
name='deleted_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contenttaxonomy',
|
||||
name='is_deleted',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='contenttaxonomy',
|
||||
name='restore_until',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user