Skip to content

Commit

Permalink
feat: Support language-specific field defaults
Browse files Browse the repository at this point in the history
Refs #700, #698
  • Loading branch information
sergei-maertens authored Sep 8, 2023
1 parent 6dc9f2a commit 2657de7
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 7 deletions.
13 changes: 13 additions & 0 deletions modeltranslation/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from django import VERSION, forms
from django.core.exceptions import ImproperlyConfigured
from django.db.models import fields
from django.utils.encoding import force_str
from django.utils.translation import override

from modeltranslation import settings as mt_settings
from modeltranslation.thread_context import fallbacks_enabled
Expand Down Expand Up @@ -256,6 +258,17 @@ def __ne__(self, other):
def __hash__(self):
return hash((self.creation_counter, self.language))

def get_default(self):
with override(self.language):
default = super().get_default()
# we must *force evaluation* at this point, otherwise the lazy translatable
# string is returned and will be evaluated only later when the main language
# is activated again (because this context block has exited).
#
# force_str passes protected types as-is, which includes None, int, float,
# datetime...
return force_str(default, strings_only=True)

def formfield(self, *args, **kwargs):
"""
Returns proper formfield, according to empty_values setting
Expand Down
11 changes: 10 additions & 1 deletion modeltranslation/tests/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 3.2.20 on 2023-09-04 07:33
# Generated by Django 3.2.21 on 2023-09-05 02:55

import django.contrib.auth.models
import django.core.validators
Expand Down Expand Up @@ -625,6 +625,15 @@ class Migration(migrations.Migration):
('email', models.EmailField(blank=True, max_length=254, null=True)),
('email_de', models.EmailField(blank=True, max_length=254, null=True)),
('email_en', models.EmailField(blank=True, max_length=254, null=True)),
('dynamic_default', models.CharField(default='password', max_length=255)),
(
'dynamic_default_de',
models.CharField(default='password', max_length=255, null=True),
),
(
'dynamic_default_en',
models.CharField(default='password', max_length=255, null=True),
),
],
),
migrations.CreateModel(
Expand Down
1 change: 1 addition & 0 deletions modeltranslation/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class TestModel(models.Model):
text = models.TextField(blank=True, null=True)
url = models.URLField(blank=True, null=True)
email = models.EmailField(blank=True, null=True)
dynamic_default = models.CharField(default=gettext_lazy("password"), max_length=255)


class UniqueNullableModel(models.Model):
Expand Down
46 changes: 40 additions & 6 deletions modeltranslation/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,31 @@ def test_update_or_create_new(self):
title_de='old de',
)

def test_callable_field_default_uses_field_language(self):
# the test uses translations from django.contrib.auth django.po file by
# specifying a model default with one of the translatable literals from that
# app

# unsaved instance must follow django's behaviour for callable default
raw_instance = models.TestModel()

assert raw_instance.dynamic_default == "Passwort"
assert raw_instance.dynamic_default_en == "password"
assert raw_instance.dynamic_default_de == "Passwort"

# saved instance must have same behaviour as unsaved instance
instance = models.TestModel.objects.create()

assert instance.dynamic_default == "Passwort"
assert instance.dynamic_default_en == "password"
assert instance.dynamic_default_de == "Passwort"
assert_db_record(
instance,
dynamic_default='Passwort',
dynamic_default_en='password',
dynamic_default_de='Passwort',
)


class ModeltranslationTransactionTest(ModeltranslationTransactionTestBase):
def test_unique_nullable_field(self):
Expand Down Expand Up @@ -2408,6 +2433,8 @@ class TestModelAdmin(admin.TranslationAdmin):
'url_en',
'email_de',
'email_en',
'dynamic_default_de',
'dynamic_default_en',
)

def test_default_fieldsets(self):
Expand All @@ -2426,6 +2453,8 @@ class TestModelAdmin(admin.TranslationAdmin):
'url_en',
'email_de',
'email_en',
'dynamic_default_de',
'dynamic_default_en',
]
assert ma.get_fieldsets(request) == [(None, {'fields': fields})]
assert ma.get_fieldsets(request, self.test_obj) == [(None, {'fields': fields})]
Expand Down Expand Up @@ -2459,15 +2488,15 @@ class TestModelAdmin(admin.TranslationAdmin):

# Using `exclude`.
class TestModelAdmin(admin.TranslationAdmin):
exclude = ['url', 'email']
exclude = ['url', 'email', 'dynamic_default']

ma = TestModelAdmin(models.TestModel, self.site)
fields = ['title_de', 'title_en', 'text_de', 'text_en']
assert tuple(ma.get_form(request).base_fields.keys()) == tuple(fields)

# You can also pass a tuple to `exclude`.
class TestModelAdmin(admin.TranslationAdmin):
exclude = ('url', 'email')
exclude = ('url', 'email', 'dynamic_default')

ma = TestModelAdmin(models.TestModel, self.site)
assert tuple(ma.get_form(request).base_fields.keys()) == tuple(fields)
Expand Down Expand Up @@ -2502,6 +2531,8 @@ class TestModelAdmin(admin.TranslationAdmin):
'url_en',
'email_de',
'email_en',
'dynamic_default_de',
'dynamic_default_en',
)

# Using grouped fields.
Expand Down Expand Up @@ -2550,7 +2581,7 @@ class TestModelAdmin(admin.TranslationAdmin):
class TestModelForm(forms.ModelForm):
class Meta:
model = models.TestModel
exclude = ['url', 'email']
exclude = ['url', 'email', 'dynamic_default']

class TestModelAdmin(admin.TranslationAdmin):
form = TestModelForm
Expand All @@ -2564,7 +2595,7 @@ class TestModelAdmin(admin.TranslationAdmin):
# option, the ModelAdmin wins. This is Django behaviour.
class TestModelAdmin(admin.TranslationAdmin):
form = TestModelForm
exclude = ['url']
exclude = ['url', 'dynamic_default']

ma = TestModelAdmin(models.TestModel, self.site)
fields = ['title_de', 'title_en', 'text_de', 'text_en', 'email_de', 'email_en']
Expand Down Expand Up @@ -3658,8 +3689,11 @@ class Meta:
'email',
'email_de',
'email_en',
'dynamic_default',
'dynamic_default_de',
'dynamic_default_en',
]
assert list(form.fields) == ['title', 'text', 'url', 'email']
assert list(form.fields) == ['title', 'text', 'url', 'email', 'dynamic_default']

def test_updating_with_empty_value(self):
"""
Expand All @@ -3670,7 +3704,7 @@ def test_updating_with_empty_value(self):
class Form(forms.ModelForm):
class Meta:
model = models.TestModel
exclude = ('text',)
exclude = ('text', 'dynamic_default')

instance = models.TestModel.objects.create(text_de='something')
form = Form(
Expand Down
1 change: 1 addition & 0 deletions modeltranslation/tests/translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class TestTranslationOptions(TranslationOptions):
'text',
'url',
'email',
'dynamic_default',
)
empty_values = ''

Expand Down

0 comments on commit 2657de7

Please sign in to comment.