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
class DjangoModelMixin:
 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.

DjangoModelMixin(**params)
45    def __init__(self, **params):
46        super().__init__(**params)
47        self.instance: Model | None = None
model: ClassVar[type[django.db.models.base.Model] | None] = None
form_class: ClassVar[type | None] = None
state_fields: ClassVar[list[str]] = []
params: dict
state: dict[str, typing.Any]
instance: django.db.models.base.Model | None
def get_queryset(self) -> django.db.models.query.QuerySet:
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.

def get_instance(self) -> django.db.models.base.Model:
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.

def save_instance(self, commit: bool = True) -> django.db.models.base.Model:
 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

def get_form_class(self) -> type | None:
111    def get_form_class(self) -> type | None:
112        """Get form class for validation."""
113        return self.form_class

Get form class for validation.

def validate_and_save(self, data: dict) -> tuple[bool, dict]:
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)

def mount(self):
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.

def hydrate(self, state: dict):
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.

def populate_state_from_instance(self) -> None:
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.

def update_instance_from_state(self) -> None:
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.

class DjangoModelComponent(DjangoModelMixin):
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()