Files
Django-Translatable-Fields/django_translatable_fields/forms.py

224 lines
8.9 KiB
Python
Raw Normal View History

2025-08-02 20:08:33 +02:00
# -*- 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 <holger@backender.de>
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)