# -*- coding: utf-8 -*- """ Django Translatable Fields - Custom Managers and QuerySets This module provides custom Django managers and querysets that add powerful search and filtering capabilities for translatable fields. It includes language-aware search functionality and context management. Key Features: - Language-aware search across translatable fields - Context management for overriding browser language - Database-specific optimizations (PostgreSQL, MySQL, SQLite) - Chainable QuerySet operations with language context - Integration with global and local language contexts Usage: # Add to your model class Product(models.Model): objects = TranslatableManager() # Search in German regardless of browser language products = Product.objects.with_context(lang='de').search('test') # Search specific field products = Product.objects.search_field('name', 'hello', 'en') Author: Holger Sielaff Version: 0.1.0 """ import json from django.db import models from django.db.models import Q from django.utils.translation import get_language class TranslatableQuerySet(models.QuerySet): """ Enhanced QuerySet with language-aware search and filtering capabilities. This QuerySet extends Django's standard QuerySet with methods specifically designed for working with translatable fields. It provides: - Language context management (with_context) - Search functionality across multiple translatable fields - Single field search with language targeting - Language filtering (find records with content in specific language) - Integration with database-specific optimizations The QuerySet maintains language context across chained operations, allowing you to set a language once and have all subsequent operations respect that context. Example: # Set German context for all operations qs = Product.objects.with_context(lang='de') results = qs.filter(active=True).search('test').order_by('name') # All operations above will use German language context """ def __init__(self, *args, **kwargs): """ Initialize the QuerySet with language context support. Args: *args: Positional arguments passed to parent **kwargs: Keyword arguments passed to parent """ super().__init__(*args, **kwargs) # Store context language override - None means use Django's current language self._context_language = None def with_context(self, lang=None): """ Set language context for this QuerySet. All subsequent operations will use this language instead of the current Django language. Args: lang: Language code to use as context Returns: QuerySet with language context set Example: # Use German context for all operations products = Product.objects.with_context(lang='de') # Search will now use German by default german_products = products.search('moep') # Searches in German # Chain with other operations Product.objects.with_context(lang='de').filter(price__lt=100).search('name', 'test') """ clone = self._clone() clone._context_language = lang return clone def _get_context_language(self, language=None): """ Get the effective language to use, considering all context sources. Args: language: Explicit language parameter Returns: Language code to use """ if language is not None: return language if self._context_language is not None: return self._context_language # Check global context manager from .context import get_context_language global_context = get_context_language() if global_context is not None: return global_context return get_language() or 'en_US' def _clone(self): """Override clone to preserve context language""" clone = super()._clone() clone._context_language = getattr(self, '_context_language', None) return clone def search_translatable(self, field_name, query, language=None): """ Search in a translatable field for the specified language. Args: field_name: Name of the translatable field to search in query: Search query string language: Language code to search (defaults to context or current language) Returns: QuerySet filtered by the search criteria Example: Product.objects.search_translatable('name', 'moep', 'de') Product.objects.with_context(lang='de').search_translatable('name', 'moep') """ from .search import TranslatableSearch effective_language = self._get_context_language(language) return TranslatableSearch.search_translatable_field(self, field_name, query, effective_language) def search_translatable_all_languages(self, field_name, query): """ Search in a translatable field across all languages. Args: field_name: Name of the translatable field to search in query: Search query string Returns: QuerySet filtered by the search criteria across all languages """ if not query: return self # Search in the entire JSON field return self.filter(**{f'{field_name}__icontains': query}) def search_multiple_fields(self, query, language=None, fields=None): """ Search across multiple translatable fields. Args: query: Search query string language: Language code to search (defaults to context or current language) fields: List of field names to search (defaults to all translatable fields) Returns: QuerySet filtered by the search criteria Example: Product.objects.search_multiple_fields('moep', 'de', ['name', 'description']) Product.objects.with_context(lang='de').search_multiple_fields('moep') """ from .search import TranslatableSearch effective_language = self._get_context_language(language) return TranslatableSearch.search_multiple_fields(self, query, effective_language, fields) def filter_by_language(self, language=None): """ Filter objects that have content in the specified language. Args: language: Language code to filter by (defaults to context or current language) Returns: QuerySet filtered to objects with content in the specified language Example: Product.objects.filter_by_language('de') Product.objects.with_context(lang='de').filter_by_language() """ effective_language = self._get_context_language(language) # Get all translatable fields translatable_fields = self._get_translatable_fields() q_objects = Q() for field_name in translatable_fields: # Check if field has content for this language field_q = Q(**{f'{field_name}__icontains': f'"{effective_language}":'}) q_objects |= field_q return self.filter(q_objects) def _get_translatable_fields(self): """ Get list of translatable field names for this model. Returns: List of field names that are translatable """ if not self.model: return [] translatable_fields = [] for field in self.model._meta.get_fields(): # Check if field has translatable attribute if hasattr(field, 'translatable') and field.translatable: translatable_fields.append(field.name) return translatable_fields class TranslatableManager(models.Manager): """ Custom manager for models with translatable fields """ def get_queryset(self): """Return custom QuerySet""" return TranslatableQuerySet(self.model, using=self._db) def with_context(self, lang=None): """ Set language context for all subsequent operations. Args: lang: Language code to use as context Returns: QuerySet with language context set Example: Product.objects.with_context(lang='de').search('moep') """ return self.get_queryset().with_context(lang) def search(self, query, language=None, fields=None): """ Convenience method for searching translatable fields. Args: query: Search query string language: Language code to search (defaults to context or current language) fields: List of field names to search (defaults to all translatable fields) Returns: QuerySet filtered by the search criteria Example: Product.objects.search('moep', 'de') Product.objects.with_context(lang='de').search('moep') """ return self.get_queryset().search_multiple_fields(query, language, fields) def search_field(self, field_name, query, language=None): """ Convenience method for searching a specific translatable field. Args: field_name: Name of the translatable field to search in query: Search query string language: Language code to search (defaults to context or current language) Returns: QuerySet filtered by the search criteria Example: Product.objects.search_field('name', 'moep', 'de') Product.objects.with_context(lang='de').search_field('name', 'moep') """ return self.get_queryset().search_translatable(field_name, query, language) def with_language(self, language=None): """ Get objects that have content in the specified language. Args: language: Language code to filter by (defaults to context or current language) Returns: QuerySet filtered to objects with content in the specified language """ return self.get_queryset().filter_by_language(language) # Mixin to easily add translatable search to any model class TranslatableModelMixin: """ Mixin to add translatable search capabilities to any model. Just inherit from this mixin in your model. """ objects = TranslatableManager() class Meta: abstract = True