""" Base Admin Mixins for account and site/sector filtering. ADMIN DELETE FIX: - Admin can delete anything without 500 errors - Simple delete that just works """ from django.contrib import admin, messages from django.core.exceptions import PermissionDenied from django.db import models, transaction class AdminDeleteMixin: """ Mixin that provides a simple working delete action for admin. """ def get_actions(self, request): """Replace default delete_selected with simple working version""" actions = super().get_actions(request) # Remove Django's default delete that causes 500 errors if 'delete_selected' in actions: del actions['delete_selected'] # Add our simple delete action actions['simple_delete'] = ( self.__class__.simple_delete, 'simple_delete', 'Delete selected items' ) return actions def simple_delete(self, request, queryset): """ Simple delete that just works. Deletes items one by one with error handling. """ success = 0 errors = [] for obj in queryset: try: # Get object info before delete try: obj_str = str(obj) except Exception: obj_str = f'#{obj.pk}' # Just delete it - let the model handle soft vs hard delete obj.delete() success += 1 except Exception as e: errors.append(f'{obj_str}: {str(e)[:50]}') if success: self.message_user(request, f'Deleted {success} item(s).', messages.SUCCESS) if errors: self.message_user(request, f'Failed to delete {len(errors)}: {"; ".join(errors[:3])}', messages.ERROR) class AccountAdminMixin: """Mixin for admin classes that need account filtering""" def get_queryset(self, request): """Filter queryset by account""" qs = super().get_queryset(request) # Check for account field has_account_field = hasattr(qs.model, 'account') if has_account_field: # Superuser and developers can see all if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return qs # Filter by user's account user_account = getattr(request.user, 'account', None) if user_account: return qs.filter(account=user_account) return qs def has_view_permission(self, request, obj=None): """Check if user can view this object""" if obj: obj_account = getattr(obj, 'account', None) if obj_account: if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True user_account = getattr(request.user, 'account', None) if user_account: return obj_account == user_account return super().has_view_permission(request, obj) def has_change_permission(self, request, obj=None): """Check if user can change this object""" if obj: obj_account = getattr(obj, 'account', None) if obj_account: if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True user_account = getattr(request.user, 'account', None) if user_account: return obj_account == user_account return super().has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): """Check if user can delete this object""" if obj: obj_account = getattr(obj, 'account', None) if obj_account: if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True user_account = getattr(request.user, 'account', None) if user_account: return obj_account == user_account return super().has_delete_permission(request, obj) class SiteSectorAdminMixin: """Mixin for admin classes that need site/sector filtering""" def get_queryset(self, request): """Filter queryset by site/sector access""" qs = super().get_queryset(request) if hasattr(qs.model, 'site') and hasattr(qs.model, 'sector'): # Superuser and developers can see all if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return qs # Filter by accessible sites if hasattr(request.user, 'get_accessible_sites'): accessible_sites = request.user.get_accessible_sites() return qs.filter(site__in=accessible_sites) return qs def has_view_permission(self, request, obj=None): """Check if user can view this object""" if obj and hasattr(obj, 'site'): if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True if hasattr(request.user, 'get_accessible_sites'): accessible_sites = request.user.get_accessible_sites() return obj.site in accessible_sites return super().has_view_permission(request, obj) def has_change_permission(self, request, obj=None): """Check if user can change this object""" if obj and hasattr(obj, 'site'): if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True if hasattr(request.user, 'get_accessible_sites'): accessible_sites = request.user.get_accessible_sites() return obj.site in accessible_sites return super().has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): """Check if user can delete this object""" if obj and hasattr(obj, 'site'): if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()): return True if hasattr(request.user, 'get_accessible_sites'): accessible_sites = request.user.get_accessible_sites() return obj.site in accessible_sites return super().has_delete_permission(request, obj) # ============================================================================ # Custom ModelAdmin for Sidebar Fix + Delete Fix # ============================================================================ from unfold.admin import ModelAdmin as UnfoldModelAdmin class Igny8ModelAdmin(AdminDeleteMixin, UnfoldModelAdmin): """ Custom ModelAdmin that: 1. Fixes delete actions (no 500 errors, bypasses PROTECT if needed) 2. Ensures sidebar_navigation is set correctly on ALL pages 3. Uses dropdown filters with Apply button AdminDeleteMixin provides: - simple_delete: Safe delete (soft delete if available) """ # Enable "Apply Filters" button for dropdown filters list_filter_submit = True def _inject_sidebar_context(self, request, extra_context=None): """Helper to inject custom sidebar into context""" if extra_context is None: extra_context = {} # Get our custom sidebar from the admin site from igny8_core.admin.site import admin_site # CRITICAL: Get the full Unfold context (includes all branding, form classes, etc.) # This is what makes the logo/title appear properly unfold_context = admin_site.each_context(request) # Get the current path to detect active group current_path = request.path sidebar_navigation = admin_site.get_sidebar_list(request) # Detect active group and expand it by setting collapsible=False for group in sidebar_navigation: group_is_active = False for item in group.get('items', []): # Unfold stores resolved link in 'link_callback', original lambda in 'link' item_link = item.get('link_callback') or item.get('link', '') # Convert to string (handles lazy proxy objects and ensures it's a string) try: item_link = str(item_link) if item_link else '' except: item_link = '' # Skip if it's a function representation (e.g., "") if item_link.startswith('<'): continue # Check if current path matches this item's link if item_link and current_path.startswith(item_link): item['active'] = True group_is_active = True # If any item in this group is active, expand the group if group_is_active: group['collapsible'] = False # Expanded state else: group['collapsible'] = True # Collapsed state # Merge Unfold context with our custom sidebar unfold_context['sidebar_navigation'] = sidebar_navigation unfold_context['available_apps'] = admin_site.get_app_list(request, app_label=None) unfold_context['app_list'] = unfold_context['available_apps'] # Merge with any existing extra_context unfold_context.update(extra_context) return unfold_context def changelist_view(self, request, extra_context=None): """Override to inject custom sidebar""" extra_context = self._inject_sidebar_context(request, extra_context) return super().changelist_view(request, extra_context) def change_view(self, request, object_id, form_url='', extra_context=None): """Override to inject custom sidebar""" extra_context = self._inject_sidebar_context(request, extra_context) return super().change_view(request, object_id, form_url, extra_context) def add_view(self, request, form_url='', extra_context=None): """Override to inject custom sidebar""" extra_context = self._inject_sidebar_context(request, extra_context) return super().add_view(request, form_url, extra_context)