224 lines
8.9 KiB
Python
224 lines
8.9 KiB
Python
|
|
# -*- 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)
|