models delte and trash and cascade issues fixes

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 17:37:21 +00:00
parent 4ba3870878
commit ad828a9fcd
5 changed files with 198 additions and 12 deletions

View File

@@ -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:
obj.restore()
restored_count += 1
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,14 +195,71 @@ 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
self.message_user(
request,
f'Permanently deleted {count} record(s). This cannot be undone.',
messages.SUCCESS
)
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 {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)

View File

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

View File

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

View File

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

View File

@@ -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),
),
]