# -*- coding: utf-8 -*- """ Django Translatable Fields - Form Field Definitions This module provides custom Django form fields for handling translatable field data in forms and admin interfaces. These form fields work together with the translatable widgets to provide seamless translation editing. Key Features: - Custom form field validation for translation dictionaries - Raw database value retrieval for proper modal population - Automatic widget selection based on field type - Integration with Django admin forms - Data preparation and processing for translation widgets Author: Holger Sielaff Version: 0.1.0 """ import json from django import forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from .widgets import TranslatableTextWidget, TranslatableTextareaWidget class TranslatableFormField(forms.CharField): """ Form field that handles translatable data properly """ def __init__(self, widget_class=TranslatableTextWidget, *args, **kwargs): # Always use our custom widget, ignore any provided widget # Create widget without widget_class parameter since we changed the API if widget_class == TranslatableTextWidget: self.widget_instance = TranslatableTextWidget() elif widget_class == TranslatableTextareaWidget: self.widget_instance = TranslatableTextareaWidget() else: self.widget_instance = widget_class() kwargs['widget'] = self.widget_instance super().__init__(*args, **kwargs) def get_bound_field(self, form, field_name): """Override to provide instance data to widget and get raw JSON""" bound_field = super().get_bound_field(form, field_name) # Pass instance information to widget and get raw JSON from DB if hasattr(form, 'instance') and form.instance and hasattr(form.instance, 'pk') and form.instance.pk: # Get the raw JSON value directly from database try: from django.db import connection cursor = connection.cursor() table_name = form.instance._meta.db_table cursor.execute(f'SELECT {field_name} FROM {table_name} WHERE id = %s', [form.instance.pk]) row = cursor.fetchone() if row and row[0]: raw_json = row[0] print(f"Form field get_bound_field - got raw JSON for {field_name}: {repr(raw_json)}") # Store this for prepare_value to use self._raw_db_value = raw_json # Also pass to widget self.widget_instance._instance_data = { 'instance': form.instance, 'field_name': field_name, 'raw_json': raw_json } else: print(f"Form field get_bound_field - no raw data found for {field_name}") self._raw_db_value = None except Exception as e: print(f"Form field get_bound_field - DB error: {e}") self._raw_db_value = None return bound_field def to_python(self, value): """Convert form input to Python value""" # Don't call super().to_python(value) - it will try to validate the dict if not value: return {} if isinstance(value, dict): # Filter out empty values return {k: v for k, v in value.items() if v.strip()} if value else {} if isinstance(value, str): try: parsed = json.loads(value) if isinstance(parsed, dict): # Filter out empty values return {k: v for k, v in parsed.items() if v.strip()} if parsed else {} else: return {'en': value} if value.strip() else {} except (json.JSONDecodeError, ValueError): return {'en': value} if value.strip() else {} return {'en': str(value)} if str(value).strip() else {} def run_validators(self, value): """Override to run validators on individual translations instead of the dict""" if isinstance(value, dict): # Run validators on each translation individually for lang_code, translation in value.items(): if translation and translation.strip(): # Run validators on each translation string for validator in self.validators: validator(translation) else: # For non-dict values, use standard validation super().run_validators(value) def validate(self, value): """Validate the translatable value""" # Basic format validation only - validators are handled in run_validators if value and not isinstance(value, dict): raise ValidationError(_('Invalid translation format - must be a dictionary')) if value: for lang_code, translation in value.items(): if not isinstance(lang_code, str): raise ValidationError(_('Language code must be a string')) if not isinstance(translation, str): raise ValidationError(_('Translation must be a string')) def prepare_value(self, value): """Prepare value for widget display - use raw JSON if available""" print(f"Form field prepare_value - received: {repr(value)}, type: {type(value)}") # Use raw DB value if we have it if hasattr(self, '_raw_db_value') and self._raw_db_value: try: parsed = json.loads(self._raw_db_value) if isinstance(parsed, dict): print(f"Form field prepare_value - using raw DB dict: {parsed}") return parsed except (json.JSONDecodeError, ValueError): print(f"Form field prepare_value - raw DB value parse failed") # Fallback to original logic if not value: return {} if isinstance(value, str): try: parsed = json.loads(value) if isinstance(parsed, dict): print(f"Form field prepare_value - returning parsed dict: {parsed}") return parsed except (json.JSONDecodeError, ValueError): pass result = {'en': value} print(f"Form field prepare_value - returning english dict: {result}") return result if isinstance(value, dict): print(f"Form field prepare_value - returning dict as-is: {value}") return value result = {'en': str(value)} print(f"Form field prepare_value - returning converted dict: {result}") return result class TranslatableCharFormField(TranslatableFormField): """Form field for translatable char fields""" def __init__(self, *args, **kwargs): super().__init__(widget_class=TranslatableTextWidget, *args, **kwargs) class TranslatableTextFormField(TranslatableFormField): """Form field for translatable text fields""" def __init__(self, *args, **kwargs): super().__init__(widget_class=TranslatableTextareaWidget, *args, **kwargs) class TranslatableUnifiedFormField(TranslatableFormField): """Unified form field that adapts widget type based on field parameters""" def __init__(self, is_charfield=False, max_length=None, *args, **kwargs): # Determine widget type based on field characteristics if is_charfield or max_length: widget_class = TranslatableTextWidget else: widget_class = TranslatableTextareaWidget try: super().__init__(widget_class=widget_class, *args, **kwargs) except TypeError: kwargs.pop('allow_unicode', None) super().__init__(widget_class=widget_class, *args, **kwargs) if max_length: self.max_length = max_length class TranslatableEmailFormField(TranslatableFormField): """Form field specifically for translatable email fields""" def __init__(self, *args, **kwargs): super().__init__(widget_class=TranslatableTextWidget, *args, **kwargs) class TranslatableSlugFormField(TranslatableFormField): """Form field specifically for translatable slug fields""" def __init__(self, *args, allow_unicode=True, **kwargs): super().__init__(widget_class=TranslatableTextWidget, *args, **kwargs) class TranslatableURLFormField(TranslatableFormField): """Form field specifically for translatable URL fields""" def __init__(self, *args, **kwargs): super().__init__(widget_class=TranslatableTextWidget, *args, **kwargs)