129 lines
5.7 KiB
Python
129 lines
5.7 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
Django Translatable Fields - Migration Operations
|
||
|
|
|
||
|
|
This module provides custom Django migration operations for handling
|
||
|
|
translatable field data transitions when the translatable parameter
|
||
|
|
changes from True to False or vice versa.
|
||
|
|
|
||
|
|
Key Features:
|
||
|
|
- Automatic data conversion between JSON and string formats
|
||
|
|
- Support for translate=True to translate=False transitions
|
||
|
|
- Support for translate=False to translate=True transitions
|
||
|
|
- Language-aware data extraction and wrapping
|
||
|
|
- Reversible migration operations
|
||
|
|
- Database-safe SQL operations
|
||
|
|
|
||
|
|
Author: Holger Sielaff <holger@backender.de>
|
||
|
|
Version: 0.1.0
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
from django.db import migrations
|
||
|
|
from django.utils.translation import get_language
|
||
|
|
|
||
|
|
|
||
|
|
class ConvertTranslatableField(migrations.Operation):
|
||
|
|
"""
|
||
|
|
Custom migration operation to convert between translatable and non-translatable formats
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, model_name, field_name, from_translatable, to_translatable, language=None):
|
||
|
|
self.model_name = model_name
|
||
|
|
self.field_name = field_name
|
||
|
|
self.from_translatable = from_translatable
|
||
|
|
self.to_translatable = to_translatable
|
||
|
|
self.language = language or get_language() or 'en'
|
||
|
|
|
||
|
|
def state_forwards(self, app_label, state):
|
||
|
|
# No state changes needed - field definition stays the same
|
||
|
|
pass
|
||
|
|
|
||
|
|
def database_forwards(self, app_label, schema_editor, from_state, to_state):
|
||
|
|
"""Convert data when applying migration"""
|
||
|
|
# Get the model
|
||
|
|
from_model = from_state.apps.get_model(app_label, self.model_name)
|
||
|
|
|
||
|
|
if self.from_translatable and not self.to_translatable:
|
||
|
|
# Convert from JSON to string (extract current language)
|
||
|
|
self._convert_json_to_string(from_model, schema_editor)
|
||
|
|
elif not self.from_translatable and self.to_translatable:
|
||
|
|
# Convert from string to JSON (wrap in language dict)
|
||
|
|
self._convert_string_to_json(from_model, schema_editor)
|
||
|
|
|
||
|
|
def database_backwards(self, app_label, schema_editor, from_state, to_state):
|
||
|
|
"""Convert data when reversing migration"""
|
||
|
|
# Reverse the conversion
|
||
|
|
to_model = to_state.apps.get_model(app_label, self.model_name)
|
||
|
|
|
||
|
|
if self.from_translatable and not self.to_translatable:
|
||
|
|
# Reverse: string back to JSON
|
||
|
|
self._convert_string_to_json(to_model, schema_editor)
|
||
|
|
elif not self.from_translatable and self.to_translatable:
|
||
|
|
# Reverse: JSON back to string
|
||
|
|
self._convert_json_to_string(to_model, schema_editor)
|
||
|
|
|
||
|
|
def _convert_json_to_string(self, model, schema_editor):
|
||
|
|
"""Convert JSON translations to single language string"""
|
||
|
|
print(f"Converting {self.model_name}.{self.field_name} from JSON to string (language: {self.language})")
|
||
|
|
|
||
|
|
for obj in model.objects.all():
|
||
|
|
field_value = getattr(obj, self.field_name)
|
||
|
|
|
||
|
|
if field_value:
|
||
|
|
try:
|
||
|
|
if isinstance(field_value, str):
|
||
|
|
# Try to parse JSON
|
||
|
|
translations = json.loads(field_value)
|
||
|
|
if isinstance(translations, dict):
|
||
|
|
# Extract value for current language or fallback
|
||
|
|
new_value = (
|
||
|
|
translations.get(self.language) or
|
||
|
|
translations.get('en') or
|
||
|
|
next(iter(translations.values()), '')
|
||
|
|
)
|
||
|
|
|
||
|
|
# Update using raw SQL to avoid field processing
|
||
|
|
table_name = obj._meta.db_table
|
||
|
|
schema_editor.execute(
|
||
|
|
f"UPDATE {table_name} SET {self.field_name} = %s WHERE id = %s",
|
||
|
|
[new_value, obj.pk]
|
||
|
|
)
|
||
|
|
print(f" Converted {obj.pk}: {field_value} -> {new_value}")
|
||
|
|
|
||
|
|
except (json.JSONDecodeError, TypeError):
|
||
|
|
# Already a string, leave as-is
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _convert_string_to_json(self, model, schema_editor):
|
||
|
|
"""Convert single language string to JSON translations"""
|
||
|
|
print(f"Converting {self.model_name}.{self.field_name} from string to JSON (language: {self.language})")
|
||
|
|
|
||
|
|
for obj in model.objects.all():
|
||
|
|
field_value = getattr(obj, self.field_name)
|
||
|
|
|
||
|
|
if field_value:
|
||
|
|
try:
|
||
|
|
# Check if it's already JSON
|
||
|
|
json.loads(field_value)
|
||
|
|
# If parsing succeeds, it's already JSON - skip
|
||
|
|
except (json.JSONDecodeError, TypeError):
|
||
|
|
# It's a plain string, convert to JSON
|
||
|
|
new_value = json.dumps({self.language: str(field_value)})
|
||
|
|
|
||
|
|
# Update using raw SQL to avoid field processing
|
||
|
|
table_name = obj._meta.db_table
|
||
|
|
schema_editor.execute(
|
||
|
|
f"UPDATE {table_name} SET {self.field_name} = %s WHERE id = %s",
|
||
|
|
[new_value, obj.pk]
|
||
|
|
)
|
||
|
|
print(f" Converted {obj.pk}: {field_value} -> {new_value}")
|
||
|
|
|
||
|
|
def describe(self):
|
||
|
|
direction = "translatable" if self.to_translatable else "non-translatable"
|
||
|
|
return f"Convert {self.model_name}.{self.field_name} to {direction}"
|
||
|
|
|
||
|
|
@property
|
||
|
|
def migration_name_fragment(self):
|
||
|
|
direction = "translatable" if self.to_translatable else "nontranslatable"
|
||
|
|
return f"convert_{self.field_name}_to__{direction}"
|