1import sqlalchemy as sa
2from sqlalchemy.ext.compiler import compiles
3from sqlalchemy.ext.hybrid import hybrid_property
4from sqlalchemy.sql.expression import ColumnElement
5
6from .exceptions import ImproperlyConfigured
7
8try:
9 import babel
10 import babel.dates
11except ImportError:
12 babel = None
13
14
15def get_locale():
16 try:
17 return babel.Locale('en')
18 except AttributeError:
19 # As babel is optional, we may raise an AttributeError accessing it
20 raise ImproperlyConfigured(
21 'Could not load get_locale function using Babel. Either '
22 'install Babel or make a similar function and override it '
23 'in this module.'
24 )
25
26
27def cast_locale(obj, locale, attr):
28 """
29 Cast given locale to string. Supports also callbacks that return locales.
30
31 :param obj:
32 Object or class to use as a possible parameter to locale callable
33 :param locale:
34 Locale object or string or callable that returns a locale.
35 """
36 if callable(locale):
37 try:
38 locale = locale(obj, attr.key)
39 except TypeError:
40 try:
41 locale = locale(obj)
42 except TypeError:
43 locale = locale()
44 if isinstance(locale, babel.Locale):
45 return str(locale)
46 return locale
47
48
49class cast_locale_expr(ColumnElement):
50 inherit_cache = False
51
52 def __init__(self, cls, locale, attr):
53 self.cls = cls
54 self.locale = locale
55 self.attr = attr
56
57
58@compiles(cast_locale_expr)
59def compile_cast_locale_expr(element, compiler, **kw):
60 locale = cast_locale(element.cls, element.locale, element.attr)
61 if isinstance(locale, str):
62 return f"'{locale}'"
63 return compiler.process(locale)
64
65
66class TranslationHybrid:
67 def __init__(self, current_locale, default_locale, default_value=None):
68 if babel is None:
69 raise ImproperlyConfigured(
70 'You need to install babel in order to use TranslationHybrid.'
71 )
72 self.current_locale = current_locale
73 self.default_locale = default_locale
74 self.default_value = default_value
75
76 def getter_factory(self, attr):
77 """
78 Return a hybrid_property getter function for given attribute. The
79 returned getter first checks if object has translation for current
80 locale. If not it tries to get translation for default locale. If there
81 is no translation found for default locale it returns None.
82 """
83
84 def getter(obj):
85 current_locale = cast_locale(obj, self.current_locale, attr)
86 try:
87 return getattr(obj, attr.key)[current_locale]
88 except (TypeError, KeyError):
89 default_locale = cast_locale(obj, self.default_locale, attr)
90 try:
91 return getattr(obj, attr.key)[default_locale]
92 except (TypeError, KeyError):
93 return self.default_value
94
95 return getter
96
97 def setter_factory(self, attr):
98 def setter(obj, value):
99 if getattr(obj, attr.key) is None:
100 setattr(obj, attr.key, {})
101 locale = cast_locale(obj, self.current_locale, attr)
102 getattr(obj, attr.key)[locale] = value
103
104 return setter
105
106 def expr_factory(self, attr):
107 def expr(cls):
108 cls_attr = getattr(cls, attr.key)
109 current_locale = cast_locale_expr(cls, self.current_locale, attr)
110 default_locale = cast_locale_expr(cls, self.default_locale, attr)
111 return sa.func.coalesce(cls_attr[current_locale], cls_attr[default_locale])
112
113 return expr
114
115 def __call__(self, attr):
116 return hybrid_property(
117 fget=self.getter_factory(attr),
118 fset=self.setter_factory(attr),
119 expr=self.expr_factory(attr),
120 )