From aaf49614f28be6b3df06f9e5b48863e46f7db246 Mon Sep 17 00:00:00 2001 From: seroy Date: Fri, 14 Jul 2017 15:07:48 +0300 Subject: [PATCH 1/9] improve functionality of required parameter --- localized_fields/fields/field.py | 46 ++++++++++++--------------- localized_fields/fields/file_field.py | 2 -- localized_fields/forms.py | 7 ++-- tests/test_form.py | 2 +- 4 files changed, 24 insertions(+), 33 deletions(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 321a8d7..d23e2dd 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -32,6 +32,13 @@ class LocalizedField(HStoreField): super(LocalizedField, self).__init__(*args, **kwargs) + if self.required is None and self.blank: + self.required = [] + elif self.required is None and not self.blank: + self.required = [settings.LANGUAGE_CODE] + elif type(self.required) == bool and self.required: + self.required = [lang_code for lang_code, _ in settings.LANGUAGES] + def contribute_to_class(self, model, name, **kwargs): """Adds this field to the specifed model. @@ -185,8 +192,8 @@ class LocalizedField(HStoreField): return value def validate(self, value: LocalizedValue, *_): - """Validates that the value for the primary language - has been filled in. + """Validates that the values has been filled in for all required + languages Exceptions are raises in order to notify the user of invalid values. @@ -199,17 +206,14 @@ class LocalizedField(HStoreField): if self.null: return - primary_lang_val = getattr(value, settings.LANGUAGE_CODE) + for lang in self.required: + lang_val = getattr(value, settings.LANGUAGE_CODE) - # NOTE(seroy): use check for None, instead of `not primary_lang_val` - # condition, cause in this way we can not save '' value - if primary_lang_val is None: - raise IntegrityError( - 'null value in column "%s.%s" violates not-null constraint' % ( - self.name, - settings.LANGUAGE_CODE - ) - ) + # NOTE(seroy): use check for None, instead of `not lang_val` + # condition, cause in this way we can not save '' value + if lang_val is None: + raise IntegrityError('null value in column "%s.%s" violates' \ + 'not-null constraint' % (self.name, lang)) def formfield(self, **kwargs): """Gets the form field associated with this field.""" @@ -217,18 +221,10 @@ class LocalizedField(HStoreField): defaults = { 'form_class': LocalizedFieldForm } - + if issubclass(kwargs.get('form_class', LocalizedFieldForm), + LocalizedFieldForm): + defaults.update({ + 'required_langs': self.required + }) defaults.update(kwargs) return super().formfield(**defaults) - - def deconstruct(self): - """Gets the values to pass to :see:__init__ when - re-creating this object.""" - - name, path, args, kwargs = super( - LocalizedField, self).deconstruct() - - if self.uniqueness: - kwargs['uniqueness'] = self.uniqueness - - return name, path, args, kwargs diff --git a/localized_fields/fields/file_field.py b/localized_fields/fields/file_field.py index 49bbf86..e6777a8 100644 --- a/localized_fields/fields/file_field.py +++ b/localized_fields/fields/file_field.py @@ -146,8 +146,6 @@ class LocalizedFileField(LocalizedField): def formfield(self, **kwargs): defaults = {'form_class': LocalizedFileFieldForm} - if 'initial' in kwargs: - defaults['required'] = False defaults.update(kwargs) return super().formfield(**defaults) diff --git a/localized_fields/forms.py b/localized_fields/forms.py index c227ba3..4efa1e6 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -19,16 +19,13 @@ class LocalizedFieldForm(forms.MultiValueField): field_class = forms.fields.CharField value_class = LocalizedValue - def __init__(self, *args, **kwargs): + def __init__(self, *args, required_langs: List[str]=[], **kwargs): """Initializes a new instance of :see:LocalizedFieldForm.""" fields = [] for lang_code, _ in settings.LANGUAGES: - field_options = {'required': False} - - if lang_code == settings.LANGUAGE_CODE: - field_options['required'] = kwargs.get('required', True) + field_options = {'required': lang_code in required_langs} field_options['label'] = lang_code fields.append(self.field_class(**field_options)) diff --git a/tests/test_form.py b/tests/test_form.py index a5b1157..556a8b3 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -12,7 +12,7 @@ class LocalizedFieldFormTestCase(TestCase): """Tests whether the constructor correctly creates a field for every language.""" - form = LocalizedFieldForm() + form = LocalizedFieldForm(required_langs=[settings.LANGUAGE_CODE]) for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): assert field.label == lang_code From aff41f671a72ce2141baa36439ec402d3798d1f7 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sat, 15 Jul 2017 13:59:39 +0300 Subject: [PATCH 2/9] Implement code review suggestions for #29 --- localized_fields/fields/field.py | 2 +- localized_fields/forms.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index d23e2dd..303e294 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -36,7 +36,7 @@ class LocalizedField(HStoreField): self.required = [] elif self.required is None and not self.blank: self.required = [settings.LANGUAGE_CODE] - elif type(self.required) == bool and self.required: + elif self.required is True: self.required = [lang_code for lang_code, _ in settings.LANGUAGES] def contribute_to_class(self, model, name, **kwargs): diff --git a/localized_fields/forms.py b/localized_fields/forms.py index 4efa1e6..fb8e1c3 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -25,9 +25,11 @@ class LocalizedFieldForm(forms.MultiValueField): fields = [] for lang_code, _ in settings.LANGUAGES: - field_options = {'required': lang_code in required_langs} + field_options = dict( + required=lang_code in required_langs, + label=lang_code + ) - field_options['label'] = lang_code fields.append(self.field_class(**field_options)) super(LocalizedFieldForm, self).__init__( @@ -35,9 +37,10 @@ class LocalizedFieldForm(forms.MultiValueField): require_all_fields=False, *args, **kwargs ) + # set 'required' attribute for each widget separately - for f, w in zip(self.fields, self.widget.widgets): - w.is_required = f.required + for field, widget in zip(self.fields, self.widget.widgets): + widget.is_required = field.required def compress(self, value: List[str]) -> value_class: """Compresses the values from individual fields From 5ac05efbd0eb4f873dfcd8182fd1396fc243d96f Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sat, 15 Jul 2017 14:04:09 +0300 Subject: [PATCH 3/9] Slight clean up, use `dict` instead of {} --- localized_fields/fields/field.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 303e294..804dfcc 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -177,9 +177,6 @@ class LocalizedField(HStoreField): # are any of the language fiels None/empty? is_all_null = True for lang_code, _ in settings.LANGUAGES: - # NOTE(seroy): use check for None, instead of - # `bool(value.get(lang_code))==True` condition, cause in this way - # we can not save '' value if value.get(lang_code) is not None: is_all_null = False break @@ -209,8 +206,6 @@ class LocalizedField(HStoreField): for lang in self.required: lang_val = getattr(value, settings.LANGUAGE_CODE) - # NOTE(seroy): use check for None, instead of `not lang_val` - # condition, cause in this way we can not save '' value if lang_val is None: raise IntegrityError('null value in column "%s.%s" violates' \ 'not-null constraint' % (self.name, lang)) @@ -218,13 +213,11 @@ class LocalizedField(HStoreField): def formfield(self, **kwargs): """Gets the form field associated with this field.""" - defaults = { - 'form_class': LocalizedFieldForm - } - if issubclass(kwargs.get('form_class', LocalizedFieldForm), - LocalizedFieldForm): - defaults.update({ - 'required_langs': self.required - }) + defaults = dict(form_class=LocalizedFieldForm) + + form_class = kwargs.get('form_class', LocalizedFieldForm) + if issubclass(form_class, LocalizedFieldForm): + defaults.update(dict(required_langs=self.required)) + defaults.update(kwargs) return super().formfield(**defaults) From 08690ab361522480464e3f0912369d70d40655f4 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sat, 15 Jul 2017 14:06:50 +0300 Subject: [PATCH 4/9] Fix outstanding PEP8 and Flake8 issues --- localized_fields/fields/field.py | 2 +- tests/test_file_field.py | 13 ++++++------- tests/test_file_field_form.py | 3 +-- tests/test_slug_fields.py | 3 +-- tests/test_value.py | 2 -- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 804dfcc..9adb446 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -207,7 +207,7 @@ class LocalizedField(HStoreField): lang_val = getattr(value, settings.LANGUAGE_CODE) if lang_val is None: - raise IntegrityError('null value in column "%s.%s" violates' \ + raise IntegrityError('null value in column "%s.%s" violates ' 'not-null constraint' % (self.name, lang)) def formfield(self, **kwargs): diff --git a/tests/test_file_field.py b/tests/test_file_field.py index 923ebb8..529564e 100644 --- a/tests/test_file_field.py +++ b/tests/test_file_field.py @@ -72,7 +72,7 @@ class LocalizedFileFieldTestCase(TestCase): @classmethod def test_save_form_data(cls): - """Tests whether the :see:save_form_data function correctly set + """Tests whether the :see:save_form_data function correctly set a valid value.""" instance = cls.FileFieldModel() @@ -87,7 +87,7 @@ class LocalizedFileFieldTestCase(TestCase): instance = cls.FileFieldModel() instance.file = {'en': ContentFile("test", "testfilename")} instance._meta.get_field('file').pre_save(instance, False) - assert instance.file.en._committed == True + assert instance.file.en._committed is True @classmethod def test_file_methods(cls): @@ -135,17 +135,17 @@ class LocalizedFileFieldTestCase(TestCase): @staticmethod def test_get_prep_value(): - """Tests whether the :see:get_prep_value function returns correctly + """Tests whether the :see:get_prep_value function returns correctly value.""" value = LocalizedValue({'en': None}) - assert LocalizedFileField().get_prep_value(None) == None + assert LocalizedFileField().get_prep_value(None) is None assert isinstance(LocalizedFileField().get_prep_value(value), dict) assert LocalizedFileField().get_prep_value(value)['en'] == '' @staticmethod def test_formfield(): - """Tests whether the :see:formfield function correctly returns + """Tests whether the :see:formfield function correctly returns a valid form.""" form_field = LocalizedFileField().formfield() @@ -161,6 +161,5 @@ class LocalizedFileFieldTestCase(TestCase): name, path, args, kwargs = LocalizedFileField().deconstruct() assert 'upload_to' in kwargs assert 'storage' not in kwargs - name, path, \ - args, kwargs = LocalizedFileField(storage='test').deconstruct() + name, path, args, kwargs = LocalizedFileField(storage='test').deconstruct() assert 'storage' in kwargs diff --git a/tests/test_file_field_form.py b/tests/test_file_field_form.py index 3b33ba4..74dcc7c 100644 --- a/tests/test_file_field_form.py +++ b/tests/test_file_field_form.py @@ -28,7 +28,7 @@ class LocalizedFileFieldFormTestCase(TestCase): formfield.clean(['', ''], ['', '']) def test_bound_data(self): - """Tests whether the :see:bound_data function is returns correctly + """Tests whether the :see:bound_data function is returns correctly value""" formfield = LocalizedFileFieldForm() @@ -38,4 +38,3 @@ class LocalizedFileFieldFormTestCase(TestCase): value = [None] * len(settings.LANGUAGES) expected_value = [''] * len(settings.LANGUAGES) assert formfield.bound_data(value, initial) == expected_value - diff --git a/tests/test_slug_fields.py b/tests/test_slug_fields.py index afd45ea..4d2d172 100644 --- a/tests/test_slug_fields.py +++ b/tests/test_slug_fields.py @@ -124,7 +124,7 @@ class LocalizedSlugFieldTestCase(TestCase): def generate_slug(instance): return instance.title - model = get_fake_model({ + get_fake_model({ 'title': LocalizedField(), 'slug': LocalizedUniqueSlugField(populate_from=generate_slug) }) @@ -138,7 +138,6 @@ class LocalizedSlugFieldTestCase(TestCase): for lang_code, lang_name in settings.LANGUAGES: assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() - @staticmethod def test_populate_multiple_from_fields(): """Tests whether populating the slug from multiple diff --git a/tests/test_value.py b/tests/test_value.py index 4d0fe61..844bac0 100644 --- a/tests/test_value.py +++ b/tests/test_value.py @@ -149,8 +149,6 @@ class LocalizedValueTestCase(TestCase): __str__'s fallback functionality properly respects the LOCALIZED_FIELDS_FALLBACKS setting.""" - test_value = 'myvalue' - settings.LOCALIZED_FIELDS_FALLBACKS = { 'nl': ['ro'] } From cff388cae329adf8b86b392e60ca5665d9392069 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sat, 15 Jul 2017 14:07:40 +0300 Subject: [PATCH 5/9] Add PEP8 and Flake8 as part of Scrutinizer test --- .scrutinizer.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 5635672..bb1d0ff 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -13,17 +13,20 @@ filter: build: environment: python: '3.5.0' - node: 'v6.2.0' variables: DJANGO_SETTINGS_MODULES: settings DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields postgresql: true - redis: true + dependencies: override: - 'pip install -r requirements/test.txt' tests: override: + - + command: pep8 ./localized_fields/ + - + command: flake8 ./localized_fields/ - command: coverage run manage.py test coverage: From 7d629c186dc18771bff30e0d98cd1fa17ad7199b Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Sun, 16 Jul 2017 12:22:48 +0300 Subject: [PATCH 6/9] Add basic tests for 'required' attribute --- tests/test_field.py | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/test_field.py b/tests/test_field.py index b998a46..809ff04 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -9,6 +9,7 @@ from localized_fields.forms import LocalizedFieldForm from localized_fields.value import LocalizedValue from .data import get_init_values +from .fake_model import get_fake_model class LocalizedFieldTestCase(TestCase): @@ -156,3 +157,62 @@ class LocalizedFieldTestCase(TestCase): LocalizedField().formfield(), LocalizedFieldForm ) + + def test_required_all(self): + """Tests whether passing required=True properly validates + that all languages are filled in.""" + + model = get_fake_model(dict( + title=LocalizedField(required=True) + )) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(ro='romanian', nl='dutch')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(nl='dutch')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(random='random')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict()) + + with self.assertRaises(IntegrityError): + model.objects.create(title=None) + + with self.assertRaises(IntegrityError): + model.objects.create(title='') + + with self.assertRaises(IntegrityError): + model.objects.create(title=' ') + + def test_required_some(self): + """Tests whether passing an array to required, + properly validates whether the specified languages + are marked as required.""" + + model = get_fake_model(dict( + title=LocalizedField(required=['nl', 'ro']) + )) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(ro='romanian', nl='dutch')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(nl='dutch')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict(random='random')) + + with self.assertRaises(IntegrityError): + model.objects.create(title=dict()) + + with self.assertRaises(IntegrityError): + model.objects.create(title=None) + + with self.assertRaises(IntegrityError): + model.objects.create(title='') + + with self.assertRaises(IntegrityError): + model.objects.create(title=' ') From c4bf151938f7f4cb9123c443341fa2a3cd4a83d7 Mon Sep 17 00:00:00 2001 From: seroy Date: Mon, 17 Jul 2017 20:53:50 +0300 Subject: [PATCH 7/9] Refactor required_langs into required --- localized_fields/fields/field.py | 18 ++++++++---------- localized_fields/forms.py | 9 +++++---- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 9adb446..6a57a2f 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -1,6 +1,6 @@ import json -from typing import Union +from typing import Union, List from django.conf import settings from django.db.utils import IntegrityError @@ -27,12 +27,12 @@ class LocalizedField(HStoreField): # The descriptor to use for accessing the attribute off of the class. descriptor_class = LocalizedValueDescriptor - def __init__(self, *args, **kwargs): + def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs): """Initializes a new instance of :see:LocalizedField.""" - super(LocalizedField, self).__init__(*args, **kwargs) + super(LocalizedField, self).__init__(*args, required=required, **kwargs) - if self.required is None and self.blank: + if (self.required is None and self.blank) or self.required is False: self.required = [] elif self.required is None and not self.blank: self.required = [settings.LANGUAGE_CODE] @@ -213,11 +213,9 @@ class LocalizedField(HStoreField): def formfield(self, **kwargs): """Gets the form field associated with this field.""" - defaults = dict(form_class=LocalizedFieldForm) - - form_class = kwargs.get('form_class', LocalizedFieldForm) - if issubclass(form_class, LocalizedFieldForm): - defaults.update(dict(required_langs=self.required)) - + defaults = dict( + form_class=LocalizedFieldForm, + required=False if self.blank else self.required + ) defaults.update(kwargs) return super().formfield(**defaults) diff --git a/localized_fields/forms.py b/localized_fields/forms.py index fb8e1c3..21606cf 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from django import forms from django.conf import settings @@ -19,21 +19,22 @@ class LocalizedFieldForm(forms.MultiValueField): field_class = forms.fields.CharField value_class = LocalizedValue - def __init__(self, *args, required_langs: List[str]=[], **kwargs): + def __init__(self, *args, required: Union[bool, List[str]]=False, **kwargs): """Initializes a new instance of :see:LocalizedFieldForm.""" fields = [] for lang_code, _ in settings.LANGUAGES: field_options = dict( - required=lang_code in required_langs, + required=required if type(required) is bool else (lang_code in + required), label=lang_code ) - fields.append(self.field_class(**field_options)) super(LocalizedFieldForm, self).__init__( fields, + required=required if type(required) is bool else True, require_all_fields=False, *args, **kwargs ) From 33e97093738cf1949c1320a456e9f2cac65db5f4 Mon Sep 17 00:00:00 2001 From: seroy Date: Mon, 17 Jul 2017 20:55:13 +0300 Subject: [PATCH 8/9] Add tests for 'required' attribute --- tests/test_field.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_form.py | 23 ++++++++++++++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/tests/test_field.py b/tests/test_field.py index 809ff04..3bd896c 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -15,6 +15,25 @@ from .fake_model import get_fake_model class LocalizedFieldTestCase(TestCase): """Tests the :see:LocalizedField class.""" + @staticmethod + def test_init(): + """Tests whether the :see:__init__ function + correctly handles parameters""" + + field = LocalizedField(blank=True) + assert field.required == [] + + field = LocalizedField(blank=False) + assert field.required == [settings.LANGUAGE_CODE] + + field = LocalizedField(required=True) + assert field.required == [lang_code for lang_code, _ in + settings.LANGUAGES] + + field = LocalizedField(required=False) + assert field.required == [] + + @staticmethod def test_from_db_value(): """Tests whether the :see:from_db_value function @@ -158,6 +177,34 @@ class LocalizedFieldTestCase(TestCase): LocalizedFieldForm ) + # case optional filling + field = LocalizedField(blank=True, required=[]) + assert not field.formfield().required + for field in field.formfield().fields: + assert not field.required + + # case required for any language + field = LocalizedField(blank=False, required=[]) + assert field.formfield().required + for field in field.formfield().fields: + assert not field.required + + # case required for specific languages + required_langs = ['ro', 'nl'] + field = LocalizedField(blank=False, required=required_langs) + assert field.formfield().required + for field in field.formfield().fields: + if field.label in required_langs: + assert field.required + else: + assert not field.required + + # case required for all languages + field = LocalizedField(blank=False, required=True) + assert field.formfield().required + for field in field.formfield().fields: + assert field.required + def test_required_all(self): """Tests whether passing required=True properly validates that all languages are filled in.""" diff --git a/tests/test_form.py b/tests/test_form.py index 556a8b3..0bedcc1 100644 --- a/tests/test_form.py +++ b/tests/test_form.py @@ -11,8 +11,8 @@ class LocalizedFieldFormTestCase(TestCase): def test_init(): """Tests whether the constructor correctly creates a field for every language.""" - - form = LocalizedFieldForm(required_langs=[settings.LANGUAGE_CODE]) + # case required for specific language + form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE]) for (lang_code, _), field in zip(settings.LANGUAGES, form.fields): assert field.label == lang_code @@ -22,6 +22,25 @@ class LocalizedFieldFormTestCase(TestCase): else: assert not field.required + # case required for all languages + form = LocalizedFieldForm(required=True) + assert form.required + for field in form.fields: + assert field.required + + # case optional filling + form = LocalizedFieldForm(required=False) + assert not form.required + for field in form.fields: + assert not field.required + + # case required for any language + form = LocalizedFieldForm(required=[]) + assert form.required + for field in form.fields: + assert not field.required + + @staticmethod def test_compress(): """Tests whether the :see:compress function From 60d14069d8de352c9d42da4db9e2fff0eb1a327d Mon Sep 17 00:00:00 2001 From: seroy Date: Mon, 17 Jul 2017 20:57:03 +0300 Subject: [PATCH 9/9] Updated 'required' attribute in documentation --- README.rst | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b6628a6..8d0e73e 100644 --- a/README.rst +++ b/README.rst @@ -129,21 +129,42 @@ Constraints **Required/Optional** -At the moment, it is not possible to select two languages to be marked as required. The constraint is **not** enforced on a database level. +Constraints is enforced on a database level. -* Make the primary language **required** and the others optional (this is the **default**): +* Optional filling .. code-block:: python class MyModel(models.Model): - title = LocalizedField(required=True) + title = LocalizedField(blank=True, null=True, required=False) -* Make all languages optional: +* Make translation required for any language .. code-block:: python class MyModel(models.Model): - title = LocalizedField(null=True) + title = LocalizedField(blank=False, null=False, required=False) + +* Make translation required for specific languages + + .. code-block:: python + + class MyModel(models.Model): + title = LocalizedField(blank=False, null=False, required=['en', 'ro']) + +* Make translation required for all languages + + .. code-block:: python + + class MyModel(models.Model): + title = LocalizedField(blank=False, null=False, required=True) + +* By default the primary language **required** and the others optional: + + .. code-block:: python + + class MyModel(models.Model): + title = LocalizedField() **Uniqueness**