Files
Django-Translatable-Fields/django_translatable_fields/managers.py
Holger Sielaff 79c68169f6 Initial
2025-08-02 20:08:33 +02:00

313 lines
11 KiB
Python

# -*- 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 <holger@backender.de>
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