Files
WYGIWYH/app/apps/common/fields/forms/dynamic_select.py
Herculino Trotta 6955294283 feat(datepicker): drop native datepickers in favor of AirDatePicker for better compatibility
As Firefox (still) doesn't support month input type
2025-01-14 23:47:03 -03:00

166 lines
5.8 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from apps.common.widgets.tom_select import TomSelect, TomSelectMultiple
class DynamicModelChoiceField(forms.ModelChoiceField):
def __init__(self, model, *args, **kwargs):
self.model = model
self.to_field_name = kwargs.pop("to_field_name", "pk")
self.create_field = kwargs.pop("create_field", None)
if not self.create_field:
raise ValueError("The 'create_field' parameter is required.")
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, *args, **kwargs)
self._created_instance = None
self.widget = TomSelect(clear_button=True, create=True)
def to_python(self, value):
if value in self.empty_values:
return None
try:
return self.model.objects.get(**{self.to_field_name: value})
except (ValueError, TypeError, self.model.DoesNotExist):
return value # Return the raw value; we'll handle creation in clean()
def clean(self, value):
if value in self.empty_values:
if self.required:
raise ValidationError(self.error_messages["required"], code="required")
return None
if isinstance(value, self.model):
return value
if isinstance(value, str):
value = value.strip()
if not value:
if self.required:
raise ValidationError(
self.error_messages["required"], code="required"
)
return None
try:
if value.isdigit():
return self.model.objects.get(id=value)
else:
raise self.model.DoesNotExist
except self.model.DoesNotExist:
try:
with transaction.atomic():
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
)
self._created_instance = instance
return instance
except Exception as e:
raise ValidationError(
self.error_messages["invalid_choice"], code="invalid_choice"
)
return super().clean(value)
def bound_data(self, data, initial):
if self._created_instance and isinstance(data, str):
if data == self._created_instance.name:
return self._created_instance.pk
return super().bound_data(data, initial)
class DynamicModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
A custom ModelMultipleChoiceField that creates new entries if they don't exist.
This field allows users to select multiple existing options or add new ones.
If a selected option doesn't exist, it will be created in the database.
Attributes:
create_field (str): The name of the field to use when creating new instances.
"""
def __init__(self, model, **kwargs):
"""
Initialize the CreateIfNotExistsModelMultipleChoiceField.
Args:
create_field (str): The name of the field to use when creating new instances.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
self.create_field = kwargs.pop("create_field", None)
if not self.create_field:
raise ValueError("The 'create_field' parameter is required.")
self.model = model
self.queryset = kwargs.pop("queryset", model.objects.all())
super().__init__(queryset=self.queryset, **kwargs)
self.widget = TomSelectMultiple(
remove_button=True, clear_button=True, create=True, checkboxes=True
)
def _create_new_instance(self, value):
"""
Create a new instance of the model with the given value.
Args:
value: The value to use for creating the new instance.
Returns:
Model: The newly created model instance.
Raises:
ValidationError: If there's an error creating the new instance.
"""
try:
with transaction.atomic():
instance, _ = self.model.objects.update_or_create(
**{self.create_field: value}
)
return instance
except Exception as e:
raise ValidationError(_("Error creating new instance"))
def clean(self, value):
"""
Clean and validate the field value.
This method checks if each selected choice exists in the database.
If a choice doesn't exist, it creates a new instance of the model.
Args:
value (list): List of selected values.
Returns:
list: A list containing all selected and newly created model instances.
Raises:
ValidationError: If there's an error during the cleaning process.
"""
if not value:
return []
string_values = set(str(v) for v in value)
existing_objects = list(
self.queryset.filter(**{f"{self.create_field}__in": string_values})
)
existing_values = set(
str(getattr(obj, self.create_field)) for obj in existing_objects
)
new_values = string_values - existing_values
new_objects = []
for new_value in new_values:
try:
new_objects.append(self._create_new_instance(new_value))
except ValidationError as e:
raise ValidationError(_("Error creating new instance"))
return existing_objects + new_objects