component_framework.adapters.django_model
Django model integration mixin.
1"""Django model integration mixin.""" 2 3from typing import Any, ClassVar 4 5try: 6 from django.db import transaction 7 from django.db.models import Model, QuerySet 8except ImportError as e: 9 from . import _require_extra 10 11 raise _require_extra("django", "django") from e 12 13 14class DjangoModelMixin: 15 """ 16 Mixin for Django model integration with components. 17 18 Provides: 19 - Instance loading with query optimization 20 - Save with transaction support 21 - Validation integration 22 - Related object prefetching 23 24 Designed for use with ``Component`` which provides ``params`` and ``state``. 25 """ 26 27 # Django model class 28 model: ClassVar[type[Model] | None] = None 29 30 # Query optimization 31 select_related: ClassVar[list[str]] = [] 32 prefetch_related: ClassVar[list[str]] = [] 33 34 # Form/validation 35 form_class: ClassVar[type | None] = None 36 37 # Fields to sync between model instance and component state 38 state_fields: ClassVar[list[str]] = [] 39 40 # Provided by cooperating Component base class 41 params: dict 42 state: dict[str, Any] 43 44 def __init__(self, **params): 45 super().__init__(**params) 46 self.instance: Model | None = None 47 48 # ---------- Instance Management ---------- 49 50 def get_queryset(self) -> QuerySet: 51 """ 52 Get base queryset with optimizations. 53 54 Override to add filters, annotations, etc. 55 """ 56 if not self.model: 57 raise ValueError("model attribute is required") 58 59 qs = self.model.objects.all() # type: ignore[unresolved-attribute] # Django Manager descriptor 60 61 if self.select_related: 62 qs = qs.select_related(*self.select_related) 63 64 if self.prefetch_related: 65 qs = qs.prefetch_related(*self.prefetch_related) 66 67 return qs 68 69 def get_instance(self) -> Model: 70 """ 71 Load model instance. 72 73 Looks for 'pk' or 'id' in params. 74 Returns new instance if not found. 75 """ 76 if not self.model: 77 raise ValueError("model attribute is required") 78 79 pk = self.params.get("pk") or self.params.get("id") 80 81 if pk: 82 try: 83 return self.get_queryset().get(pk=pk) 84 except self.model.DoesNotExist: # type: ignore[unresolved-attribute] # Django metaclass attr 85 raise ValueError(f"{self.model.__name__} with pk={pk} not found") 86 87 return self.model() 88 89 def save_instance(self, commit: bool = True) -> Model: 90 """ 91 Save model instance. 92 93 Args: 94 commit: Whether to save to database 95 96 Returns: 97 Saved or unsaved model instance 98 """ 99 if not self.instance: 100 raise ValueError("No instance to save") 101 102 if commit: 103 with transaction.atomic(): 104 self.instance.save() 105 106 return self.instance 107 108 # ---------- Form Integration ---------- 109 110 def get_form_class(self) -> type | None: 111 """Get form class for validation.""" 112 return self.form_class 113 114 def validate_and_save(self, data: dict) -> tuple[bool, dict]: 115 """ 116 Validate data and save instance. 117 118 Args: 119 data: Form data to validate 120 121 Returns: 122 Tuple of (success, errors_dict) 123 """ 124 form_class = self.get_form_class() 125 if not form_class: 126 # No form validation, update instance directly 127 for key, value in data.items(): 128 if hasattr(self.instance, key): 129 setattr(self.instance, key, value) 130 self.save_instance() 131 return True, {} 132 133 # Use Django form for validation 134 form = form_class(data, instance=self.instance) 135 if form.is_valid(): 136 self.instance = form.save() 137 return True, {} 138 else: 139 return False, form.errors.as_dict() 140 141 # ---------- Lifecycle Integration ---------- 142 143 def mount(self): 144 """Load instance on mount.""" 145 super().mount() # type: ignore[unresolved-attribute] # cooperative mixin; Component provides mount() 146 self.instance = self.get_instance() 147 self.populate_state_from_instance() 148 149 def hydrate(self, state: dict): 150 """Load instance on hydrate.""" 151 super().hydrate(state) # type: ignore[unresolved-attribute] # cooperative mixin; Component provides hydrate() 152 # Reload instance if PK in state 153 if "pk" in state: 154 self.params["pk"] = state["pk"] 155 self.instance = self.get_instance() 156 157 def populate_state_from_instance(self) -> None: 158 """ 159 Populate component state from model instance. 160 161 Override to customize which fields are included. 162 """ 163 if not self.instance: 164 return 165 166 # Add PK if instance is saved 167 if self.instance.pk: 168 self.state["pk"] = self.instance.pk 169 170 # Add fields defined in state_fields 171 for field_name in self.state_fields: 172 value = getattr(self.instance, field_name, None) 173 self.state[field_name] = value 174 175 def update_instance_from_state(self) -> None: 176 """ 177 Update model instance from component state. 178 179 Override to customize field mapping. 180 """ 181 if not self.instance: 182 return 183 184 for field_name in self.state_fields: 185 if field_name in self.state: 186 setattr(self.instance, field_name, self.state[field_name]) 187 188 189class DjangoModelComponent(DjangoModelMixin): 190 """ 191 Complete model-bound component. 192 193 Usage: 194 @registry.register("order_form") 195 class OrderForm(DjangoModelComponent): 196 model = Order 197 state_fields = ["status", "customer", "total"] 198 select_related = ["customer"] 199 template_name = "components/order_form.html" 200 201 def on_save(self): 202 self.update_instance_from_state() 203 self.save_instance() 204 """ 205 206 pass
15class DjangoModelMixin: 16 """ 17 Mixin for Django model integration with components. 18 19 Provides: 20 - Instance loading with query optimization 21 - Save with transaction support 22 - Validation integration 23 - Related object prefetching 24 25 Designed for use with ``Component`` which provides ``params`` and ``state``. 26 """ 27 28 # Django model class 29 model: ClassVar[type[Model] | None] = None 30 31 # Query optimization 32 select_related: ClassVar[list[str]] = [] 33 prefetch_related: ClassVar[list[str]] = [] 34 35 # Form/validation 36 form_class: ClassVar[type | None] = None 37 38 # Fields to sync between model instance and component state 39 state_fields: ClassVar[list[str]] = [] 40 41 # Provided by cooperating Component base class 42 params: dict 43 state: dict[str, Any] 44 45 def __init__(self, **params): 46 super().__init__(**params) 47 self.instance: Model | None = None 48 49 # ---------- Instance Management ---------- 50 51 def get_queryset(self) -> QuerySet: 52 """ 53 Get base queryset with optimizations. 54 55 Override to add filters, annotations, etc. 56 """ 57 if not self.model: 58 raise ValueError("model attribute is required") 59 60 qs = self.model.objects.all() # type: ignore[unresolved-attribute] # Django Manager descriptor 61 62 if self.select_related: 63 qs = qs.select_related(*self.select_related) 64 65 if self.prefetch_related: 66 qs = qs.prefetch_related(*self.prefetch_related) 67 68 return qs 69 70 def get_instance(self) -> Model: 71 """ 72 Load model instance. 73 74 Looks for 'pk' or 'id' in params. 75 Returns new instance if not found. 76 """ 77 if not self.model: 78 raise ValueError("model attribute is required") 79 80 pk = self.params.get("pk") or self.params.get("id") 81 82 if pk: 83 try: 84 return self.get_queryset().get(pk=pk) 85 except self.model.DoesNotExist: # type: ignore[unresolved-attribute] # Django metaclass attr 86 raise ValueError(f"{self.model.__name__} with pk={pk} not found") 87 88 return self.model() 89 90 def save_instance(self, commit: bool = True) -> Model: 91 """ 92 Save model instance. 93 94 Args: 95 commit: Whether to save to database 96 97 Returns: 98 Saved or unsaved model instance 99 """ 100 if not self.instance: 101 raise ValueError("No instance to save") 102 103 if commit: 104 with transaction.atomic(): 105 self.instance.save() 106 107 return self.instance 108 109 # ---------- Form Integration ---------- 110 111 def get_form_class(self) -> type | None: 112 """Get form class for validation.""" 113 return self.form_class 114 115 def validate_and_save(self, data: dict) -> tuple[bool, dict]: 116 """ 117 Validate data and save instance. 118 119 Args: 120 data: Form data to validate 121 122 Returns: 123 Tuple of (success, errors_dict) 124 """ 125 form_class = self.get_form_class() 126 if not form_class: 127 # No form validation, update instance directly 128 for key, value in data.items(): 129 if hasattr(self.instance, key): 130 setattr(self.instance, key, value) 131 self.save_instance() 132 return True, {} 133 134 # Use Django form for validation 135 form = form_class(data, instance=self.instance) 136 if form.is_valid(): 137 self.instance = form.save() 138 return True, {} 139 else: 140 return False, form.errors.as_dict() 141 142 # ---------- Lifecycle Integration ---------- 143 144 def mount(self): 145 """Load instance on mount.""" 146 super().mount() # type: ignore[unresolved-attribute] # cooperative mixin; Component provides mount() 147 self.instance = self.get_instance() 148 self.populate_state_from_instance() 149 150 def hydrate(self, state: dict): 151 """Load instance on hydrate.""" 152 super().hydrate(state) # type: ignore[unresolved-attribute] # cooperative mixin; Component provides hydrate() 153 # Reload instance if PK in state 154 if "pk" in state: 155 self.params["pk"] = state["pk"] 156 self.instance = self.get_instance() 157 158 def populate_state_from_instance(self) -> None: 159 """ 160 Populate component state from model instance. 161 162 Override to customize which fields are included. 163 """ 164 if not self.instance: 165 return 166 167 # Add PK if instance is saved 168 if self.instance.pk: 169 self.state["pk"] = self.instance.pk 170 171 # Add fields defined in state_fields 172 for field_name in self.state_fields: 173 value = getattr(self.instance, field_name, None) 174 self.state[field_name] = value 175 176 def update_instance_from_state(self) -> None: 177 """ 178 Update model instance from component state. 179 180 Override to customize field mapping. 181 """ 182 if not self.instance: 183 return 184 185 for field_name in self.state_fields: 186 if field_name in self.state: 187 setattr(self.instance, field_name, self.state[field_name])
Mixin for Django model integration with components.
Provides:
- Instance loading with query optimization
- Save with transaction support
- Validation integration
- Related object prefetching
Designed for use with Component which provides params and state.
51 def get_queryset(self) -> QuerySet: 52 """ 53 Get base queryset with optimizations. 54 55 Override to add filters, annotations, etc. 56 """ 57 if not self.model: 58 raise ValueError("model attribute is required") 59 60 qs = self.model.objects.all() # type: ignore[unresolved-attribute] # Django Manager descriptor 61 62 if self.select_related: 63 qs = qs.select_related(*self.select_related) 64 65 if self.prefetch_related: 66 qs = qs.prefetch_related(*self.prefetch_related) 67 68 return qs
Get base queryset with optimizations.
Override to add filters, annotations, etc.
70 def get_instance(self) -> Model: 71 """ 72 Load model instance. 73 74 Looks for 'pk' or 'id' in params. 75 Returns new instance if not found. 76 """ 77 if not self.model: 78 raise ValueError("model attribute is required") 79 80 pk = self.params.get("pk") or self.params.get("id") 81 82 if pk: 83 try: 84 return self.get_queryset().get(pk=pk) 85 except self.model.DoesNotExist: # type: ignore[unresolved-attribute] # Django metaclass attr 86 raise ValueError(f"{self.model.__name__} with pk={pk} not found") 87 88 return self.model()
Load model instance.
Looks for 'pk' or 'id' in params. Returns new instance if not found.
90 def save_instance(self, commit: bool = True) -> Model: 91 """ 92 Save model instance. 93 94 Args: 95 commit: Whether to save to database 96 97 Returns: 98 Saved or unsaved model instance 99 """ 100 if not self.instance: 101 raise ValueError("No instance to save") 102 103 if commit: 104 with transaction.atomic(): 105 self.instance.save() 106 107 return self.instance
Save model instance.
Arguments:
- commit: Whether to save to database
Returns:
Saved or unsaved model instance
111 def get_form_class(self) -> type | None: 112 """Get form class for validation.""" 113 return self.form_class
Get form class for validation.
115 def validate_and_save(self, data: dict) -> tuple[bool, dict]: 116 """ 117 Validate data and save instance. 118 119 Args: 120 data: Form data to validate 121 122 Returns: 123 Tuple of (success, errors_dict) 124 """ 125 form_class = self.get_form_class() 126 if not form_class: 127 # No form validation, update instance directly 128 for key, value in data.items(): 129 if hasattr(self.instance, key): 130 setattr(self.instance, key, value) 131 self.save_instance() 132 return True, {} 133 134 # Use Django form for validation 135 form = form_class(data, instance=self.instance) 136 if form.is_valid(): 137 self.instance = form.save() 138 return True, {} 139 else: 140 return False, form.errors.as_dict()
Validate data and save instance.
Arguments:
- data: Form data to validate
Returns:
Tuple of (success, errors_dict)
144 def mount(self): 145 """Load instance on mount.""" 146 super().mount() # type: ignore[unresolved-attribute] # cooperative mixin; Component provides mount() 147 self.instance = self.get_instance() 148 self.populate_state_from_instance()
Load instance on mount.
150 def hydrate(self, state: dict): 151 """Load instance on hydrate.""" 152 super().hydrate(state) # type: ignore[unresolved-attribute] # cooperative mixin; Component provides hydrate() 153 # Reload instance if PK in state 154 if "pk" in state: 155 self.params["pk"] = state["pk"] 156 self.instance = self.get_instance()
Load instance on hydrate.
158 def populate_state_from_instance(self) -> None: 159 """ 160 Populate component state from model instance. 161 162 Override to customize which fields are included. 163 """ 164 if not self.instance: 165 return 166 167 # Add PK if instance is saved 168 if self.instance.pk: 169 self.state["pk"] = self.instance.pk 170 171 # Add fields defined in state_fields 172 for field_name in self.state_fields: 173 value = getattr(self.instance, field_name, None) 174 self.state[field_name] = value
Populate component state from model instance.
Override to customize which fields are included.
176 def update_instance_from_state(self) -> None: 177 """ 178 Update model instance from component state. 179 180 Override to customize field mapping. 181 """ 182 if not self.instance: 183 return 184 185 for field_name in self.state_fields: 186 if field_name in self.state: 187 setattr(self.instance, field_name, self.state[field_name])
Update model instance from component state.
Override to customize field mapping.
190class DjangoModelComponent(DjangoModelMixin): 191 """ 192 Complete model-bound component. 193 194 Usage: 195 @registry.register("order_form") 196 class OrderForm(DjangoModelComponent): 197 model = Order 198 state_fields = ["status", "customer", "total"] 199 select_related = ["customer"] 200 template_name = "components/order_form.html" 201 202 def on_save(self): 203 self.update_instance_from_state() 204 self.save_instance() 205 """ 206 207 pass
Complete model-bound component.
Usage:
@registry.register("order_form") class OrderForm(DjangoModelComponent): model = Order state_fields = ["status", "customer", "total"] select_related = ["customer"] template_name = "components/order_form.html"
def on_save(self): self.update_instance_from_state() self.save_instance()