"""Common admin ergonomics without changing application API contracts.""" from django import forms from django.contrib import admin from django.db import models from django.core.exceptions import ValidationError from utils.admin_excel import excel_response def workspace_id(obj): if obj is None: return None if hasattr(obj, 'sys_ws_id'): return obj.sys_ws_id for relation in ('order', 'form', 'ticket_comment'): if hasattr(obj, relation): return workspace_id(getattr(obj, relation)) return None class RelatedWorkspaceForm(forms.ModelForm): def clean(self): cleaned = super().clean() workspace = cleaned.get('sys_ws') scope = workspace.pk if workspace else None if scope is None: for key in ('order', 'form', 'ticket_comment'): scope = workspace_id(cleaned.get(key)) if scope is not None: break if scope is not None: for field in self._meta.model._meta.fields: if field.is_relation and field.name != 'sys_ws': value = cleaned.get(field.name) related_scope = workspace_id(value) if related_scope is not None and related_scope != scope: self.add_error(field.name, 'این رابطه متعلق به فضای کاری دیگری است.') self.check_category_parent(cleaned) self.check_domain_values(cleaned) return cleaned def check_domain_values(self, cleaned): label = self._meta.model._meta.label_lower if label == 'cuser.cuserrole' and cleaned.get('mobile') is not None: from utils.validations.mobile import normalize_mobile try: cleaned['mobile'] = normalize_mobile(cleaned['mobile']) except ValidationError as error: self.add_error('mobile', error.messages) user = cleaned.get('cuser') if user and user.mobile != cleaned.get('mobile'): self.add_error('mobile', 'شماره نقش باید با شماره کاربر یکسان باشد.') if label in ('shop.cart', 'shop.orderitem'): key = 'quantity' if label == 'shop.cart' else 'amount' if cleaned.get(key) is not None and cleaned[key] < 1: self.add_error(key, 'تعداد باید حداقل یک باشد.') if label == 'shop.orderitem' and cleaned.get('price') is not None and cleaned['price'] < 0: self.add_error('price', 'قیمت نباید منفی باشد.') def check_category_parent(self, cleaned): if self._meta.model._meta.label_lower != 'shop.productcategory': return parent = cleaned.get('parent') visited = set() while parent is not None: if parent.pk == self.instance.pk or parent.pk in visited: self.add_error('parent', 'دسته والد نباید یک چرخه ایجاد کند.') return visited.add(parent.pk) parent = parent.parent class CRMModelAdmin(admin.ModelAdmin): form = RelatedWorkspaceForm list_per_page = 50 show_full_result_count = False save_on_top = True actions = ('export_selected',) sensitive_fields = () def __init__(self, model, admin_site): super().__init__(model, admin_site) fields = model._meta.fields names = {field.name for field in fields} if self.list_display == ('__str__',): candidates = ('id', 'name', 'name_family', 'title', 'mobile', 'customer', 'lead', 'form', 'product', 'order', 'cuser_role', 'role_type', 'sys_ws', 'status', 'type', 'is_active', 'expire', 'datetime', 'created_at') self.list_display = tuple(name for name in candidates if name in names)[:9] if not self.search_fields: self.search_fields = tuple(field.name for field in fields if isinstance(field, (models.CharField, models.TextField)) and field.name not in self.sensitive_fields) if not self.list_filter: self.list_filter = tuple(field.name for field in fields if field.name == 'sys_ws' or field.choices or isinstance(field, models.BooleanField)) self.readonly_fields = tuple(dict.fromkeys((*self.readonly_fields, *(name for name in ('created_at', 'updated_at', 'uuid') if name in names)))) self.raw_id_fields = tuple(field.name for field in fields if field.many_to_one) self.list_select_related = tuple(field.name for field in fields if field.many_to_one) if 'created_at' in names: self.date_hierarchy = 'created_at' if not self.ordering and not model._meta.ordering: self.ordering = ('-pk',) @admin.action(description='خروجی اکسل ردیف‌های انتخاب‌شده', permissions=['view']) def export_selected(self, request, queryset): fields = [field for field in self.model._meta.fields if field.name not in self.sensitive_fields] rows = queryset.values_list(*(field.attname for field in fields)).iterator() return excel_response([str(field.verbose_name) for field in fields], rows, f'{self.model._meta.model_name}.xlsx')