├── LICENSE ├── MANIFEST.in ├── README.rst ├── linguo ├── __init__.py ├── exceptions.py ├── forms.py ├── managers.py ├── models.py ├── tests │ ├── __init__.py │ ├── admin.py │ ├── forms.py │ ├── locale │ │ └── fr │ │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── models.py │ ├── settings.py │ ├── tests.py │ └── urls.py ├── utils.py └── views.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Trapeze 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of Trapeze Media nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 20 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include linguo/tests/locale/*/LC_MESSAGES/* 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained.** 2 | 3 | 4 | **Update:** Version 1.4.0 adds support for Django 1.6 and 1.7 5 | and drops support for Django < 1.4 6 | 7 | Linguo 8 | ====== 9 | 10 | Linguo aims to make model translation easy. It is designed to let you use the 11 | built-in Django features (Query API, Model Forms, Admin, etc) as intended. 12 | Linguo integrates relatively easily with your existing code and performs the 13 | translation retrieval logic transparently (similar to ugettext). It does this 14 | by creating additional columns for each language and using proxy properties to 15 | make it transparent to you. 16 | 17 | 18 | 19 | Features 20 | -------- 21 | 22 | * Automatically references the correct translation based on the current active 23 | language. 24 | * Lets you use the Django ORM normally (no need to worry about which fields are 25 | translatable, linguo figures it out for you). 26 | * Support ModelForms by automatically retrieving/saving values based on the 27 | active language. 28 | * Supports Django versions 1.4.9 to 1.7.1 29 | * Comprehensive test coverage 30 | 31 | 32 | 33 | Usage 34 | ----- 35 | 36 | Subclass ``MultilingualModel`` and define the ``translate`` property: 37 | ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 38 | 39 | :: 40 | 41 | from linguo.models import MultilingualModel 42 | from linguo.managers import MultilingualManager 43 | 44 | class Product(MultilingualModel): 45 | name = models.CharField(max_length=255, verbose_name=_('name')) 46 | description = models.TextField(verbose_name=_('description')) 47 | price = models.FloatField(verbose_name=_('price')) 48 | 49 | objects = MultilingualManager() 50 | 51 | class Meta: 52 | # name and description are translatable fields 53 | translate = ('name', 'description') 54 | 55 | ``MultilingualManager`` allows you to transparently perform filtering and 56 | ordering on translatable fields (more on this below). 57 | 58 | 59 | Assuming your ``LANGUAGES`` settings looks like this ... 60 | '''''''''''''''''''''''''''''''''''''''''''''''''''''''' 61 | :: 62 | 63 | LANGUAGES = ( 64 | ('en', ugettext('English')), 65 | ('fr', ugettext('French')), 66 | ) 67 | 68 | 69 | Then, you can do this: 70 | '''''''''''''''''''''' 71 | 72 | **Create a product:** It automatically sets the values for the current active 73 | language. 74 | :: 75 | 76 | from django.utils import translation # import the translation package 77 | 78 | translation.activate('en') 79 | product = Product.objects.create( 80 | name='English Name', 81 | description='English description', 82 | price=10 83 | ) 84 | 85 | 86 | **Translate the fields** on that product. 87 | :: 88 | 89 | product.translate(language='fr', 90 | name='French Name', description='French description' 91 | ) 92 | product.save() 93 | # You don't have to specify price, because it is not a translatable field 94 | 95 | 96 | If you **switch languages**, it will automatically retrieve the corresponding 97 | translated values. 98 | :: 99 | 100 | translation.activate('fr') 101 | 102 | product.name 103 | -> 'French Name' 104 | 105 | product.description 106 | -> 'French description' 107 | 108 | 109 | If you **modify translatable fields**, it will automatically assign it to 110 | current active language. 111 | :: 112 | 113 | translation.activate('fr') 114 | 115 | product.name = 'New French Name' 116 | product.save() 117 | 118 | translation.activate('en') 119 | 120 | product.name # This remains untouched in English 121 | -> 'English Name' 122 | 123 | 124 | Non-translated fields will have the same value regardless of the language 125 | we are operating in. 126 | :: 127 | 128 | translation.activate('en') 129 | product.price = 99 130 | product.save() 131 | 132 | translation.activate('fr') 133 | product.price 134 | -> 99 135 | 136 | 137 | Querying the database 138 | ''''''''''''''''''''' 139 | 140 | **Filtering and ordering** works as you would expect it to. It will 141 | filter/order in the language you are operating in. You need to have 142 | ``MultilingualManager`` on the model in order for this feature to work. 143 | :: 144 | 145 | translation.activate('fr') 146 | Product.objects.filter(name='French Name').order_by('name') 147 | 148 | 149 | Model Forms for Multilingual models 150 | ''''''''''''''''''''''''''''''''''' 151 | 152 | Model Forms work transparently in the sense that it automatically saves the form 153 | data to the current active language. However, if you want to edit multiple 154 | languages at the same time (eg. ``name``, ``name_fr``, etc.) see section below 155 | on 'Admin Model Forms'. :: 156 | 157 | class ProductForm(forms.ModelForm): 158 | class Meta: 159 | fields = ('name', 'description', 'price',) 160 | model = Product 161 | 162 | When saving the form, it will automatically save the form data to the fields in 163 | the **current active language**. 164 | :: 165 | 166 | translation.activate('fr') # Activate French 167 | 168 | data = {'name': 'French Name', 'description': 'French Description', 'price': 37} 169 | form = ProductForm(data=data) 170 | 171 | new_product = form.save() 172 | 173 | new_product.name 174 | -> 'French Name' 175 | 176 | new_product.description 177 | -> 'French Description' 178 | 179 | new_product.price 180 | -> 37.0 181 | 182 | 183 | # Other languages will not be affected 184 | 185 | translation.activate('en') 186 | 187 | new_product.name 188 | -> '' 189 | 190 | new_product.description 191 | -> '' 192 | 193 | new_product.price 194 | -> 37 195 | # Of course, non-translatable fields will have a consistent value 196 | 197 | 198 | Admin Model Forms (editing multiple languages at the same time) 199 | ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' 200 | In the admin, you most probably want to include fields for each language (eg. 201 | ``name``, ``name_fr``, etc.). In this case you must subclass 202 | ``MultilingualModelForm`` and use it as the admin form. 203 | :: 204 | 205 | # Form definition 206 | from linguo.forms import MultilingualModelForm 207 | 208 | class ProductAdminForm(MultilingualModelForm): 209 | class Meta: 210 | model = Product 211 | fields = forms.ALL_FIELDS 212 | 213 | # Admin definition 214 | class ProductAdmin(admin.ModelAdmin): 215 | form = ProductAdminForm 216 | 217 | 218 | ``MultilingualModelForm`` can be used anytime you want to allow editing multiple 219 | language simultaneously (not just in the admin). Basically, it just **disables 220 | the automatic routing** to the current active language. 221 | 222 | 223 | Installation 224 | ------------ 225 | 226 | #. Add ``linguo`` to your ``INSTALLED_APPS`` setting. 227 | #. Ensure the ``LANGUAGES`` setting contains all the languages for your site. 228 | 229 | 230 | Adding new languages 231 | '''''''''''''''''''' 232 | 233 | 1. Append the new language to the ``LANGUAGES`` setting. 234 | - You should avoid changing the primary language (ie. the first language in the list). If you do that, you will have to migrate the data in that column. 235 | 2. Generate migrations (since new fields will be added to your models): 236 | :: 237 | 238 | ./manage.py makemigrations 239 | 240 | 241 | Running the tests 242 | ----------------- 243 | :: 244 | 245 | ./manage.py test linguo.tests --settings=linguo.tests.settings 246 | 247 | 248 | Troubleshooting 249 | --------------- 250 | 251 | If you run into this message when generating migrations: 252 | :: 253 | 254 | $ ./manage.py schemamigration yourapp --auto 255 | ? The field 'YourModel.field_text_de' does not have a default specified, yet is NOT NULL. 256 | ? Since you are adding this field, you MUST specify a default 257 | ? value to use for existing rows. Would you like to: 258 | ? 1. Quit now, and add a default to the field in models.py 259 | ? 2. Specify a one-off value to use for existing columns now 260 | ? Please select a choice: 261 | 262 | It means you have ``blank=False, default=None`` on one or more of your models. 263 | 264 | 265 | Behind The Scenes (How It Works) 266 | -------------------------------- 267 | For each field marked as translatable, ``linguo`` will create additional 268 | database fields for each additional language. 269 | 270 | For example, if you mark the following field as translatable ... 271 | :: 272 | 273 | name = models.CharField(_('name'), max_length=255) 274 | 275 | class Meta: 276 | translate = ('name',) 277 | 278 | ... and you have three languages (en, fr, de). Your model will have the following db fields: 279 | :: 280 | 281 | name = models.CharField(_('name'), max_length=255) # This is for the FIRST language "en" 282 | name_fr = models.CharField(_('name (French)'), max_length=255) # This is for "fr" 283 | name_de = models.CharField(_('name (German)'), max_length=255) # This is for "de" 284 | 285 | On the instantiated model, "name" becomes a ``property`` that appropriately 286 | gets/sets the values for the corresponding field that matches the language we 287 | are working with. 288 | 289 | For example, if the current language is "fr" ... 290 | :: 291 | 292 | product = Product() 293 | product.name = "test" # --> sets name_fr 294 | 295 | ... this will set ``product.name_fr`` (not ``product.name``) 296 | 297 | 298 | Database filtering works because ``MultingualQueryset`` rewrites the query. 299 | 300 | For example, if the current language is "fr", and we run the following query ... 301 | :: 302 | 303 | Product.objects.filter(name="test") 304 | 305 | ... it will be rewritten to be ... 306 | :: 307 | 308 | Product.objects.filter(name_fr="test") 309 | 310 | 311 | License 312 | ------- 313 | 314 | This app is licensed under the BSD license. See the LICENSE file for details. 315 | Basically, feel free to do what you want with this code, but I'm not liable if 316 | your computer blows up. 317 | -------------------------------------------------------------------------------- /linguo/__init__.py: -------------------------------------------------------------------------------- 1 | """Linguo aims to make model translation easy and is designed to let you use the built-in Django features (Query API, Model Forms, Admin, etc) as intended.""" 2 | 3 | VERSION = (1, 4, 0) 4 | 5 | __version__ = '.'.join(map(str, VERSION)) 6 | -------------------------------------------------------------------------------- /linguo/exceptions.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import FieldError 2 | 3 | 4 | class MultilingualFieldError(FieldError): 5 | pass 6 | -------------------------------------------------------------------------------- /linguo/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.conf import settings 3 | 4 | from linguo.utils import get_normalized_language 5 | 6 | 7 | class MultilingualModelForm(forms.ModelForm): 8 | def __init__(self, data=None, files=None, instance=None, **kwargs): 9 | # We force the language to the primary, temporarily disabling the 10 | # routing based on current active language. 11 | # This allows all field values to be extracted from the model in super's init() 12 | # as it populates self.initial) 13 | 14 | if instance is not None: 15 | old_force_language = instance._force_language 16 | instance._force_language = get_normalized_language(settings.LANGUAGES[0][0]) 17 | else: 18 | old_force_language = None 19 | 20 | super(MultilingualModelForm, self).__init__( 21 | data=data, files=files, instance=instance, **kwargs 22 | ) 23 | self.instance._force_language = old_force_language 24 | 25 | def _post_clean(self): 26 | # We force the language to the primary, temporarily disabling the 27 | # routing based on current active language. 28 | # This allows all fields to be assigned to the corresponding language 29 | old_force_language = self.instance._force_language 30 | self.instance._force_language = get_normalized_language(settings.LANGUAGES[0][0]) 31 | super(MultilingualModelForm, self)._post_clean() 32 | self.instance._force_language = old_force_language 33 | -------------------------------------------------------------------------------- /linguo/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.fields.related import RelatedField 3 | from django.conf import settings 4 | from django.utils.translation import get_language 5 | 6 | from linguo.utils import get_real_field_name 7 | 8 | 9 | def rewrite_lookup_key(model, lookup_key): 10 | from linguo.models import MultilingualModel # to avoid circular import 11 | if issubclass(model, MultilingualModel): 12 | pieces = lookup_key.split('__') 13 | # If we are doing a lookup on a translatable field, we want to rewrite it to the actual field name 14 | # For example, we want to rewrite "name__startswith" to "name_fr__startswith" 15 | if pieces[0] in model._meta.translatable_fields: 16 | lookup_key = get_real_field_name(pieces[0], get_language().split('-')[0]) 17 | 18 | remaining_lookup = '__'.join(pieces[1:]) 19 | if remaining_lookup: 20 | lookup_key = '%s__%s' % (lookup_key, remaining_lookup) 21 | elif pieces[0] in map(lambda field: '%s_%s' % (field, settings.LANGUAGES[0][0]), model._meta.translatable_fields): 22 | # If the lookup field explicitly refers to the primary langauge (eg. "name_en"), 23 | # we want to rewrite that to point to the actual field name. 24 | lookup_key = pieces[0][:-3] # Strip out the language suffix 25 | remaining_lookup = '__'.join(pieces[1:]) 26 | if remaining_lookup: 27 | lookup_key = '%s__%s' % (lookup_key, remaining_lookup) 28 | 29 | pieces = lookup_key.split('__') 30 | if len(pieces) > 1: 31 | # Check if we are doing a lookup to a related trans model 32 | fields_to_trans_models = get_fields_to_translatable_models(model) 33 | for field_to_trans, transmodel in fields_to_trans_models: 34 | if pieces[0] == field_to_trans: 35 | sub_lookup = '__'.join(pieces[1:]) 36 | if sub_lookup: 37 | sub_lookup = rewrite_lookup_key(transmodel, sub_lookup) 38 | lookup_key = '%s__%s' % (pieces[0], sub_lookup) 39 | break 40 | 41 | return lookup_key 42 | 43 | 44 | def get_fields_to_translatable_models(model): 45 | results = [] 46 | from linguo.models import MultilingualModel # to avoid circular import 47 | 48 | for field_name in model._meta.get_all_field_names(): 49 | field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name) 50 | if direct and isinstance(field_object, RelatedField): 51 | if issubclass(field_object.related.parent_model, MultilingualModel): 52 | results.append((field_name, field_object.related.parent_model)) 53 | return results 54 | 55 | 56 | class MultilingualQuerySet(models.query.QuerySet): 57 | 58 | def __init__(self, *args, **kwargs): 59 | super(MultilingualQuerySet, self).__init__(*args, **kwargs) 60 | if self.model and (not self.query.order_by): 61 | if self.model._meta.ordering: 62 | # If we have default ordering specified on the model, set it now so that 63 | # it can be rewritten. Otherwise sql.compiler will grab it directly from _meta 64 | ordering = [] 65 | for key in self.model._meta.ordering: 66 | ordering.append(rewrite_lookup_key(self.model, key)) 67 | self.query.add_ordering(*ordering) 68 | 69 | def _filter_or_exclude(self, negate, *args, **kwargs): 70 | for key, val in kwargs.items(): 71 | new_key = rewrite_lookup_key(self.model, key) 72 | del kwargs[key] 73 | kwargs[new_key] = val 74 | 75 | return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) 76 | 77 | def order_by(self, *field_names): 78 | new_args = [] 79 | for key in field_names: 80 | new_args.append(rewrite_lookup_key(self.model, key)) 81 | return super(MultilingualQuerySet, self).order_by(*new_args) 82 | 83 | def update(self, **kwargs): 84 | for key, val in kwargs.items(): 85 | new_key = rewrite_lookup_key(self.model, key) 86 | del kwargs[key] 87 | kwargs[new_key] = val 88 | return super(MultilingualQuerySet, self).update(**kwargs) 89 | update.alters_data = True 90 | 91 | 92 | class MultilingualManager(models.Manager): 93 | use_for_related_fields = True 94 | 95 | def get_queryset(self): 96 | return MultilingualQuerySet(self.model) 97 | 98 | def get_query_set(self): # For Django < 1.6 compatibility 99 | return self.get_queryset() 100 | -------------------------------------------------------------------------------- /linguo/models.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from django.db import models 4 | from django.db.models.base import ModelBase 5 | from django.conf import settings 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from linguo.exceptions import MultilingualFieldError 9 | from linguo.managers import MultilingualManager 10 | from linguo.utils import get_real_field_name, get_normalized_language, get_current_language 11 | 12 | 13 | class MultilingualModelBase(ModelBase): 14 | 15 | def __new__(cls, name, bases, attrs): 16 | local_trans_fields, inherited_trans_fields = \ 17 | cls.get_trans_fields(name, bases, attrs) 18 | 19 | if ('Meta' in attrs) and hasattr(attrs['Meta'], 'translate'): 20 | delattr(attrs['Meta'], 'translate') 21 | 22 | attrs = cls.rewrite_trans_fields(local_trans_fields, attrs) 23 | attrs = cls.rewrite_unique_together(local_trans_fields, attrs) 24 | 25 | new_obj = super(MultilingualModelBase, cls).__new__(cls, name, bases, attrs) 26 | new_obj._meta.translatable_fields = inherited_trans_fields + local_trans_fields 27 | 28 | # Add a property that masks the translatable fields 29 | for field_name in local_trans_fields: 30 | # If there is already a property with the same name, we will leave it 31 | # This also happens if the Class is created multiple times 32 | # (Django's ModelBase has the ability to detect this and "bail out" but we don't) 33 | if type(new_obj.__dict__.get(field_name)) == property: 34 | continue 35 | 36 | # Some fields add a descriptor (ie. FileField), we want to keep that on the model 37 | if field_name in new_obj.__dict__: 38 | primary_lang_field_name = '%s_%s' % ( 39 | field_name, get_normalized_language(settings.LANGUAGES[0][0]) 40 | ) 41 | setattr(new_obj, primary_lang_field_name, new_obj.__dict__[field_name]) 42 | 43 | getter = cls.generate_field_getter(field_name) 44 | setter = cls.generate_field_setter(field_name) 45 | setattr(new_obj, field_name, property(getter, setter)) 46 | 47 | return new_obj 48 | 49 | @classmethod 50 | def get_trans_fields(cls, name, bases, attrs): 51 | local_trans_fields = [] 52 | inherited_trans_fields = [] 53 | 54 | if ('Meta' in attrs) and hasattr(attrs['Meta'], 'translate'): 55 | local_trans_fields = list(attrs['Meta'].translate) 56 | 57 | # Check for translatable fields in parent classes 58 | for base in bases: 59 | if hasattr(base, '_meta') and hasattr(base._meta, 'translatable_fields'): 60 | inherited_trans_fields.extend(list(base._meta.translatable_fields)) 61 | 62 | # Validate the local_trans_fields 63 | for field in local_trans_fields: 64 | if field not in attrs: 65 | raise MultilingualFieldError( 66 | '`%s` cannot be translated because it' 67 | ' is not a field on the model %s' % (field, name) 68 | ) 69 | 70 | return (local_trans_fields, inherited_trans_fields) 71 | 72 | @classmethod 73 | def rewrite_trans_fields(cls, local_trans_fields, attrs): 74 | """Create copies of the local translatable fields for each language""" 75 | for field in local_trans_fields: 76 | 77 | for lang in settings.LANGUAGES[1:]: 78 | lang_code = lang[0] 79 | 80 | lang_field = copy.copy(attrs[field]) 81 | # The new field cannot have the same creation_counter (else the ordering will be arbitrary) 82 | # We increment by a decimal point because we don't want to have 83 | # to adjust the creation_counter of ALL other subsequent fields 84 | lang_field.creation_counter += 0.0001 # Limitation this trick: only supports upto 10,000 languages 85 | lang_fieldname = get_real_field_name(field, lang_code) 86 | lang_field.name = lang_fieldname 87 | 88 | if lang_field.verbose_name is not None: 89 | # This is to extract the original value that was passed into ugettext_lazy 90 | # We do this so that we avoid evaluating the lazy object. 91 | raw_verbose_name = lang_field.verbose_name._proxy____args[0] 92 | else: 93 | raw_verbose_name = field.replace('-', ' ') 94 | lang_field.verbose_name = _(u'%(verbose_name)s (%(language)s)' % 95 | {'verbose_name': raw_verbose_name, 'language': lang[1]} 96 | ) 97 | 98 | attrs[lang_fieldname] = lang_field 99 | 100 | return attrs 101 | 102 | @classmethod 103 | def generate_field_getter(cls, field): 104 | # Property that masks the getter of a translatable field 105 | def getter(self_reference): 106 | lang = self_reference._force_language or get_current_language() 107 | attrname = '%s_%s' % (field, lang) 108 | return getattr(self_reference, attrname) 109 | return getter 110 | 111 | @classmethod 112 | def generate_field_setter(cls, field): 113 | # Property that masks a setter of the translatable field 114 | def setter(self_reference, value): 115 | lang = self_reference._force_language or get_current_language() 116 | attrname = '%s_%s' % (field, lang) 117 | setattr(self_reference, attrname, value) 118 | return setter 119 | 120 | @classmethod 121 | def rewrite_unique_together(cls, local_trans_fields, attrs): 122 | if ('Meta' not in attrs) or not hasattr(attrs['Meta'], 'unique_together'): 123 | return attrs 124 | 125 | # unique_together can be either a tuple of tuples, or a single 126 | # tuple of two strings. Normalize it to a tuple of tuples. 127 | ut = attrs['Meta'].unique_together 128 | if ut and not isinstance(ut[0], (tuple, list)): 129 | ut = (ut,) 130 | 131 | # Determine which constraints need to be rewritten 132 | new_ut = [] 133 | constraints_to_rewrite = [] 134 | for constraint in ut: 135 | needs_rewriting = False 136 | for field in constraint: 137 | if field in local_trans_fields: 138 | needs_rewriting = True 139 | break 140 | if needs_rewriting: 141 | constraints_to_rewrite.append(constraint) 142 | else: 143 | new_ut.append(constraint) 144 | 145 | # Now perform the re-writing 146 | for constraint in constraints_to_rewrite: 147 | for lang in settings.LANGUAGES: 148 | lang_code = lang[0] 149 | new_constraint = [] 150 | for field in constraint: 151 | if field in local_trans_fields: 152 | field = get_real_field_name(field, lang_code) 153 | new_constraint.append(field) 154 | 155 | new_ut.append(tuple(new_constraint)) 156 | 157 | attrs['Meta'].unique_together = tuple(new_ut) 158 | return attrs 159 | 160 | 161 | class MultilingualModel(models.Model): 162 | __metaclass__ = MultilingualModelBase 163 | 164 | objects = MultilingualManager() 165 | 166 | class Meta: 167 | abstract = True 168 | 169 | def __init__(self, *args, **kwargs): 170 | self._force_language = None 171 | 172 | # Rewrite any keyword arguments for translatable fields 173 | language = get_current_language() 174 | for field in self._meta.translatable_fields: 175 | if field in kwargs.keys(): 176 | attrname = get_real_field_name(field, language) 177 | if attrname != field: 178 | kwargs[attrname] = kwargs[field] 179 | del kwargs[field] 180 | 181 | # We have to force the primary language before initializing or else 182 | # our "proxy" property will prevent the primary language values from being returned. 183 | self._force_language = get_normalized_language(settings.LANGUAGES[0][0]) 184 | super(MultilingualModel, self).__init__(*args, **kwargs) 185 | self._force_language = None 186 | 187 | def save(self, *args, **kwargs): 188 | # We have to force the primary language before saving or else 189 | # our "proxy" property will prevent the primary language values from being returned. 190 | old_forced_language = self._force_language 191 | self._force_language = get_normalized_language(settings.LANGUAGES[0][0]) 192 | super(MultilingualModel, self).save(*args, **kwargs) 193 | # Now we can switch back 194 | self._force_language = old_forced_language 195 | 196 | def translate(self, language, **kwargs): 197 | # Temporarily force this objects language 198 | old_forced_language = self._force_language 199 | self._force_language = language 200 | # Set the values 201 | for key, val in kwargs.iteritems(): 202 | setattr(self, key, val) # Set values on the object 203 | # Now switch back 204 | self._force_language = old_forced_language 205 | -------------------------------------------------------------------------------- /linguo/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmathew/django-linguo/489fae37eddad50584b6063c8ac218d3b740a947/linguo/tests/__init__.py -------------------------------------------------------------------------------- /linguo/tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from linguo.tests.forms import MultilingualBarFormAllFields 4 | from linguo.tests.models import Bar 5 | 6 | 7 | class BarAdmin(admin.ModelAdmin): 8 | form = MultilingualBarFormAllFields 9 | list_display = ('name', 'price', 'quantity', 'description',) 10 | list_filter = ('name',) 11 | search_fields = ('name', 'description',) 12 | 13 | 14 | admin.site.register(Bar, BarAdmin) 15 | -------------------------------------------------------------------------------- /linguo/tests/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from linguo.forms import MultilingualModelForm 4 | from linguo.tests.models import Bar 5 | 6 | 7 | class BarForm(forms.ModelForm): 8 | class Meta: 9 | model = Bar 10 | if hasattr(forms, 'ALL_FIELDS'): # For Django < 1.6 compatibility 11 | fields = forms.ALL_FIELDS 12 | 13 | 14 | class BarFormWithFieldsSpecified(forms.ModelForm): 15 | class Meta: 16 | model = Bar 17 | fields = ('name', 'price', 'description', 'quantity',) 18 | 19 | 20 | class BarFormWithFieldsExcluded(forms.ModelForm): 21 | class Meta: 22 | model = Bar 23 | exclude = ('categories', 'name',) 24 | 25 | 26 | class BarFormWithCustomField(BarFormWithFieldsSpecified): 27 | custom_field = forms.CharField() 28 | 29 | 30 | class MultilingualBarFormAllFields(MultilingualModelForm): 31 | class Meta: 32 | model = Bar 33 | if hasattr(forms, 'ALL_FIELDS'): # For Django < 1.6 compatibility 34 | fields = forms.ALL_FIELDS 35 | -------------------------------------------------------------------------------- /linguo/tests/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zmathew/django-linguo/489fae37eddad50584b6063c8ac218d3b740a947/linguo/tests/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /linguo/tests/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2011-04-10 15:18-0400\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=n>1;\n" 19 | 20 | msgid "English" 21 | msgstr "Anglais" 22 | 23 | msgid "French" 24 | msgstr "Français" 25 | 26 | 27 | msgid "name" 28 | msgstr "neom" 29 | 30 | msgid "name (French)" 31 | msgstr "neom (Français)" 32 | 33 | 34 | msgid "description (French)" 35 | msgstr "déscriptione (Français)" 36 | -------------------------------------------------------------------------------- /linguo/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from linguo.managers import MultilingualManager 5 | from linguo.models import MultilingualModel 6 | 7 | 8 | class FooCategory(MultilingualModel): 9 | name = models.CharField(max_length=255, verbose_name=_('name')) 10 | 11 | objects = MultilingualManager() 12 | 13 | class Meta: 14 | ordering = ('name', 'id',) 15 | translate = ('name',) 16 | 17 | 18 | class Foo(MultilingualModel): 19 | price = models.PositiveIntegerField(verbose_name=_('price')) 20 | name = models.CharField(max_length=255, verbose_name=_('name')) 21 | categories = models.ManyToManyField(FooCategory, blank=True) 22 | 23 | objects = MultilingualManager() 24 | 25 | class Meta: 26 | translate = ('name',) 27 | unique_together = ('name', 'price',) 28 | 29 | 30 | class FooRel(models.Model): 31 | myfoo = models.ForeignKey(Foo) 32 | desc = models.CharField(max_length=255, verbose_name=_('desc')) 33 | 34 | objects = MultilingualManager() 35 | 36 | 37 | class Moo(Foo): 38 | q1 = models.PositiveIntegerField() 39 | 40 | class Meta: 41 | unique_together = ('q1',) 42 | 43 | 44 | class Bar(Foo): 45 | quantity = models.PositiveIntegerField(verbose_name=_('quantity')) 46 | description = models.CharField(max_length=255) 47 | 48 | objects = MultilingualManager() 49 | 50 | class Meta: 51 | translate = ('description',) 52 | 53 | 54 | class BarRel(models.Model): 55 | mybar = models.ForeignKey(Bar) 56 | desc = models.CharField(max_length=255, verbose_name=_('desc')) 57 | 58 | objects = MultilingualManager() 59 | 60 | 61 | class AbstractMoe(MultilingualModel): 62 | name = models.CharField(max_length=255, verbose_name=_('name')) 63 | price = models.PositiveIntegerField(verbose_name=_('price')) 64 | 65 | class Meta: 66 | abstract = True 67 | translate = ('name',) 68 | 69 | 70 | class Moe(AbstractMoe): 71 | description = models.CharField(max_length=255, 72 | verbose_name=_('description'), 73 | ) 74 | quantity = models.PositiveIntegerField(verbose_name=_('quantity')) 75 | 76 | class Meta: 77 | translate = ('description',) 78 | 79 | 80 | class Gem(MultilingualModel): 81 | gemtype = models.CharField(max_length=255, verbose_name=_('gem type'), 82 | choices=(('a', 'A Type'), ('b', 'B Type'),) 83 | ) 84 | somefoo = models.ForeignKey(Foo, null=True, blank=True) 85 | 86 | objects = MultilingualManager() 87 | 88 | class Meta: 89 | translate = ('gemtype',) 90 | 91 | 92 | class Hop(MultilingualModel): 93 | name = models.CharField(max_length=255, verbose_name=_('name')) 94 | description = models.CharField(max_length=255, 95 | verbose_name=_('description'), 96 | ) 97 | price = models.PositiveIntegerField(verbose_name=_('price')) 98 | 99 | objects = MultilingualManager() 100 | 101 | class Meta: 102 | translate = ('name', 'description',) 103 | 104 | 105 | class Ord(Foo): 106 | last_name = models.CharField(max_length=255) 107 | 108 | objects = MultilingualManager() 109 | 110 | class Meta: 111 | ordering = ('name', 'last_name', 'id',) 112 | translate = ('last_name',) 113 | 114 | def __unicode__(self): 115 | return u'%s %s' % (self.name, self.last_name) 116 | 117 | 118 | class Doc(MultilingualModel): 119 | pdf = models.FileField(upload_to='files/test/') 120 | 121 | class Meta: 122 | translate = ('pdf',) 123 | 124 | 125 | class Lan(MultilingualModel): 126 | name = models.CharField(max_length=255) 127 | language = models.CharField(max_length=255, default=None) 128 | 129 | class Meta: 130 | translate = ('name',) 131 | 132 | 133 | """ 134 | class AbstractCar(models.Model): 135 | name = models.CharField(max_length=255, verbose_name=_('name'), default=None) 136 | price = models.PositiveIntegerField(verbose_name=_('price')) 137 | 138 | class Meta: 139 | abstract = True 140 | unique_together = ('name', 'price',) 141 | 142 | 143 | class TransCar(MultilingualModel, AbstractCar): 144 | description = models.CharField(max_length=255, 145 | verbose_name=_('description'), 146 | ) 147 | quantity = models.PositiveIntegerField(verbose_name=_('quantity')) 148 | 149 | class Meta: 150 | translate = ('description',) 151 | 152 | 153 | class TransCarRel(models.Model): 154 | mytranscar = models.ForeignKey(TransCar) 155 | desc = models.CharField(max_length=255, verbose_name=_('desc')) 156 | 157 | objects = MultilingualManager() 158 | 159 | 160 | class Coo(MultilingualModel, Car): 161 | q1 = models.PositiveIntegerField() 162 | 163 | class Meta: 164 | translate = ('price',) 165 | unique_together = ('q1',) 166 | 167 | 168 | class TransAbstractCar(MultilingualModel, AbstractCar): 169 | description = models.CharField(max_length=255, 170 | verbose_name=_('description'), 171 | ) 172 | quantity = models.PositiveIntegerField(verbose_name=_('quantity')) 173 | 174 | class Meta: 175 | translate = ('price', 'description') 176 | """ -------------------------------------------------------------------------------- /linguo/tests/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.dev20150101192853. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/dev/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/dev/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 16 | 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = '3l!w(pfxxsc4w0ehus6mjmbignsw6oi(0u($bo^b$5n8ypn%oe' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | TEMPLATE_DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = ( 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'linguo', 43 | 'linguo.tests', 44 | ) 45 | 46 | MIDDLEWARE_CLASSES = ( 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ) 54 | 55 | ROOT_URLCONF = 'linguo.tests.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.i18n', 66 | 'django.template.context_processors.tz', 67 | 'django.template.context_processors.media', 68 | 'django.template.context_processors.static', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 79 | 80 | DATABASES = { 81 | 'default': { 82 | 'ENGINE': 'django.db.backends.sqlite3', 83 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 84 | } 85 | } 86 | 87 | 88 | # Internationalization 89 | # https://docs.djangoproject.com/en/dev/topics/i18n/ 90 | 91 | LANGUAGE_CODE = 'en-us' 92 | 93 | from django.utils.translation import ugettext_lazy as _ 94 | LANGUAGES = ( 95 | ('en', _('English')), 96 | ('fr', _('French')), 97 | ) 98 | 99 | TIME_ZONE = 'UTC' 100 | 101 | USE_I18N = True 102 | 103 | USE_L10N = True 104 | 105 | USE_TZ = True 106 | 107 | 108 | # Static files (CSS, JavaScript, Images) 109 | # https://docs.djangoproject.com/en/dev/howto/static-files/ 110 | 111 | STATIC_URL = '/static/' 112 | 113 | LOCALE_PATHS = ( 114 | os.path.realpath(os.path.dirname(__file__)) + '/locale/', 115 | ) 116 | -------------------------------------------------------------------------------- /linguo/tests/tests.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import django 4 | from django.contrib.auth.models import User 5 | from django.core.urlresolvers import reverse 6 | from django.db import IntegrityError 7 | from django.test import TestCase 8 | from django.utils import translation 9 | 10 | from linguo.tests.forms import BarForm, BarFormWithFieldsSpecified, \ 11 | BarFormWithFieldsExcluded, MultilingualBarFormAllFields 12 | from linguo.tests.models import Foo, FooRel, Moo, Bar, BarRel, Moe, Gem, \ 13 | FooCategory, Hop, Ord, Doc, Lan 14 | 15 | 16 | class LinguoTests(TestCase): 17 | 18 | def setUp(self): 19 | self.old_lang = translation.get_language() 20 | translation.activate('en') 21 | 22 | def tearDown(self): 23 | translation.activate(self.old_lang) 24 | 25 | 26 | class Tests(LinguoTests): 27 | 28 | def testOrderingOfFieldsWithinModel(self): 29 | expected = ['id', 'price', 'name', 'name_fr'] 30 | for i in range(len(Foo._meta.fields)): 31 | self.assertEqual(Foo._meta.fields[i].name, expected[i]) 32 | 33 | def testCreation(self): 34 | translation.activate('en') 35 | obj = Foo.objects.create(name='Foo', price=10) 36 | 37 | obj = Foo.objects.get(pk=obj.pk) 38 | self.assertEquals(obj.name, 'Foo') 39 | self.assertEquals(obj.price, 10) 40 | 41 | translation.activate('fr') 42 | Foo.objects.create(name='FrenchName', price=15) 43 | translation.activate('en') 44 | 45 | self.assertEquals(Foo.objects.count(), 2) 46 | 47 | def testTranslate(self): 48 | """ 49 | We should be able to translate fields on the object. 50 | """ 51 | obj = Foo.objects.create(name='Foo', price=10) 52 | obj.translate(name='FooFr', language='fr') 53 | obj.save() 54 | 55 | # Refresh from db 56 | obj = Foo.objects.get(id=obj.id) 57 | 58 | self.assertEquals(obj.name, 'Foo') 59 | self.assertEquals(obj.price, 10) 60 | 61 | translation.activate('fr') 62 | self.assertEquals(obj.name, 'FooFr') 63 | self.assertEquals(Foo.objects.count(), 1) 64 | 65 | def testDelayedCreation(self): 66 | obj = Foo() 67 | obj.name = 'Foo' 68 | obj.price = 10 69 | 70 | translation.activate('fr') 71 | obj.name = 'FooFr' 72 | obj.save() 73 | 74 | translation.activate('en') 75 | obj = Foo.objects.get(pk=obj.pk) 76 | self.assertEquals(obj.name, 'Foo') 77 | self.assertEquals(obj.price, 10) 78 | 79 | translation.activate('fr') 80 | self.assertEquals(obj.name, 'FooFr') 81 | self.assertEquals(obj.price, 10) 82 | 83 | def testMultipleTransFields(self): 84 | obj = Hop.objects.create(name='hop', description='desc', price=11) 85 | obj.translate(name='hop_fr', description='desc_fr', 86 | language='fr') 87 | 88 | self.assertEquals(obj.name, 'hop') 89 | self.assertEquals(obj.description, 'desc') 90 | self.assertEquals(obj.price, 11) 91 | 92 | translation.activate('fr') 93 | self.assertEquals(obj.name, 'hop_fr') 94 | self.assertEquals(obj.description, 'desc_fr') 95 | self.assertEquals(obj.price, 11) 96 | 97 | def testMultipleTransFieldsButNotSettingOneDuringCreation(self): 98 | obj = Hop.objects.create(name='hop', price=11) 99 | self.assertEquals(obj.name, 'hop') 100 | self.assertEquals(obj.price, 11) 101 | 102 | def testSwitchingActiveLanguageSetsValuesOnTranslatedFields(self): 103 | obj = Foo.objects.create(name='Foo', price=10) 104 | obj.translate(name='FooFr', language='fr') 105 | 106 | translation.activate('fr') 107 | self.assertEquals(obj.name, 'FooFr') 108 | obj.name = 'NewFooFr' 109 | 110 | translation.activate('en') 111 | self.assertEquals(obj.name, 'Foo') 112 | 113 | obj.save() 114 | 115 | # Refresh from db 116 | obj = Foo.objects.get(id=obj.id) 117 | self.assertEquals(obj.name, 'Foo') 118 | translation.activate('fr') 119 | self.assertEquals(obj.name, 'NewFooFr') 120 | 121 | def testCreateTranslationWithNewValueForNonTransField(self): 122 | """ 123 | That value of non-trans fields should be the same for all translations. 124 | """ 125 | obj = Foo.objects.create(name='Foo', price=10) 126 | obj.translate(name='FooFr', price=20, language='fr') 127 | 128 | translation.activate('fr') 129 | self.assertEquals(obj.name, 'FooFr') 130 | self.assertEquals(obj.price, 20) 131 | 132 | translation.activate('en') 133 | self.assertEquals(obj.price, 20) 134 | # Ensure no other fields were changed 135 | self.assertEquals(obj.name, 'Foo') 136 | 137 | def testQuerysetUpdate(self): 138 | obj = Foo.objects.create(name='Foo', price=10) 139 | obj.translate(name='FooFr', language='fr') 140 | obj.save() 141 | 142 | obj2 = Foo.objects.create(name='Foo2', price=13) 143 | obj2.translate(name='Foo2Fr', language='fr') 144 | obj2.save() 145 | 146 | qs = Foo.objects.all() 147 | self.assertEquals(qs.count(), 2) 148 | qs.update(name='NewFoo') 149 | 150 | # Refresh objects from db 151 | obj = Foo.objects.get(pk=obj.pk) 152 | obj2 = Foo.objects.get(pk=obj2.pk) 153 | 154 | self.assertEquals(obj.price, 10) 155 | self.assertEquals(obj.name, 'NewFoo') 156 | self.assertEquals(obj2.price, 13) 157 | self.assertEquals(obj2.name, 'NewFoo') 158 | 159 | translation.activate('fr') 160 | self.assertEquals(obj.name, 'FooFr') 161 | self.assertEquals(obj.price, 10) 162 | self.assertEquals(obj2.name, 'Foo2Fr') 163 | self.assertEquals(obj2.price, 13) 164 | 165 | def testQuerysetUpdateInOtherLanguageSetsValuesOnOtherLanguageOnly(self): 166 | obj = Foo.objects.create(name='Foo', price=10) 167 | obj.translate(name='FooFr', language='fr') 168 | obj.save() 169 | obj2 = Foo.objects.create(name='Foo2', price=13) 170 | obj2.translate(name='Foo2Fr', language='fr') 171 | obj2.save() 172 | 173 | translation.activate('fr') 174 | qs = Foo.objects.all() 175 | self.assertEquals(qs.count(), 2) 176 | qs.update(name='NewFooFr') 177 | 178 | # Refresh objects from db 179 | obj = Foo.objects.get(pk=obj.pk) 180 | obj2 = Foo.objects.get(pk=obj2.pk) 181 | 182 | self.assertEquals(obj.price, 10) 183 | self.assertEquals(obj.name, 'NewFooFr') 184 | self.assertEquals(obj2.price, 13) 185 | self.assertEquals(obj2.name, 'NewFooFr') 186 | 187 | translation.activate('en') 188 | self.assertEquals(obj.name, 'Foo') 189 | self.assertEquals(obj.price, 10) 190 | self.assertEquals(obj2.name, 'Foo2') 191 | self.assertEquals(obj2.price, 13) 192 | 193 | def testUniqueTogetherUsingTransFields(self): 194 | Foo.objects.create(name='Foo', price=10) 195 | 196 | try: # name, price are unique together 197 | Foo.objects.create(name='Foo', price=10) 198 | except IntegrityError: 199 | pass 200 | else: 201 | self.fail() 202 | 203 | def testFilteringOnTransField(self): 204 | obj = Foo.objects.create(name='English Foo', price=10) 205 | obj.translate(name='French Foo', language='fr') 206 | obj.save() 207 | 208 | qs = Foo.objects.filter(name="English Foo") 209 | self.assertEquals(qs.count(), 1) 210 | self.assertEquals(qs[0], obj) 211 | 212 | qs = Foo.objects.filter(name__startswith="English") 213 | self.assertEquals(qs.count(), 1) 214 | self.assertEquals(qs[0], obj) 215 | qs = Foo.objects.exclude(name__startswith="English") 216 | self.assertEquals(qs.count(), 0) 217 | 218 | translation.activate('fr') 219 | qs = Foo.objects.filter(name="French Foo") 220 | self.assertEquals(qs.count(), 1) 221 | self.assertEquals(qs[0], obj) 222 | 223 | qs = Foo.objects.filter(name__startswith="French") 224 | self.assertEquals(qs.count(), 1) 225 | self.assertEquals(qs[0], obj) 226 | qs = Foo.objects.exclude(name__startswith="French") 227 | self.assertEquals(qs.count(), 0) 228 | 229 | def testFilteringUsingExplicitFieldName(self): 230 | obj = Foo.objects.create(name='English Foo', price=10) 231 | obj.translate(name='French Foo', language='fr') 232 | obj.save() 233 | 234 | obj2 = Foo.objects.create(name='Another English Foo', price=20) 235 | obj2.translate(name='Another French Foo', language='fr') 236 | obj2.save() 237 | 238 | # we're in english 239 | qs = Foo.objects.filter(name_en="English Foo") 240 | self.assertEquals(qs.count(), 1) 241 | self.assertEquals(qs[0], obj) 242 | 243 | qs = Foo.objects.filter(name_en__startswith="English") 244 | self.assertEquals(qs.count(), 1) 245 | self.assertEquals(qs[0], obj) 246 | qs = Foo.objects.exclude(name_en__startswith="English") 247 | self.assertEquals(qs.count(), 1) 248 | self.assertEquals(qs[0], obj2) 249 | 250 | # try using the french field name 251 | qs = Foo.objects.filter(name_fr="French Foo") 252 | self.assertEquals(qs.count(), 1) 253 | self.assertEquals(qs[0], obj) 254 | 255 | qs = Foo.objects.filter(name_fr__startswith="French") 256 | self.assertEquals(qs.count(), 1) 257 | self.assertEquals(qs[0], obj) 258 | qs = Foo.objects.exclude(name_fr__startswith="French") 259 | self.assertEquals(qs.count(), 1) 260 | self.assertEquals(qs[0], obj2) 261 | 262 | # now try in french 263 | translation.activate('fr') 264 | qs = Foo.objects.filter(name_en="English Foo") 265 | self.assertEquals(qs.count(), 1) 266 | self.assertEquals(qs[0], obj) 267 | 268 | qs = Foo.objects.filter(name_en__startswith="English") 269 | self.assertEquals(qs.count(), 1) 270 | self.assertEquals(qs[0], obj) 271 | qs = Foo.objects.exclude(name_en__startswith="English") 272 | self.assertEquals(qs.count(), 1) 273 | self.assertEquals(qs[0], obj2) 274 | 275 | # try using the french field name 276 | qs = Foo.objects.filter(name_fr="French Foo") 277 | self.assertEquals(qs.count(), 1) 278 | self.assertEquals(qs[0], obj) 279 | 280 | qs = Foo.objects.filter(name_fr__startswith="French") 281 | self.assertEquals(qs.count(), 1) 282 | self.assertEquals(qs[0], obj) 283 | qs = Foo.objects.exclude(name_fr__startswith="French") 284 | self.assertEquals(qs.count(), 1) 285 | self.assertEquals(qs[0], obj2) 286 | 287 | def testOrderingOnTransField(self): 288 | obj = Foo.objects.create(name='English Foo', price=10) 289 | obj.translate(name='French Foo', language='fr') 290 | obj.save() 291 | 292 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 293 | obj2.translate(name='Another French Foo', language='fr') 294 | obj2.save() 295 | 296 | qs = Foo.objects.order_by('name') 297 | self.assertEquals(qs.count(), 2) 298 | self.assertEquals(qs[0], obj2) 299 | self.assertEquals(qs[1], obj) 300 | 301 | translation.activate('fr') 302 | qs = Foo.objects.order_by('name') 303 | self.assertEquals(qs.count(), 2) 304 | self.assertEquals(qs[0], obj2) 305 | self.assertEquals(qs[1], obj) 306 | self.assertEquals(qs[1].name, 'French Foo') 307 | 308 | def testDefaultOrderingIsTransField(self): 309 | """ 310 | Test a model that has a trans field in the default ordering. 311 | """ 312 | f1 = FooCategory.objects.create(name='B2 foo') 313 | f1.translate(name='B2 foo', language='fr') 314 | f1.save() 315 | 316 | f2 = FooCategory.objects.create(name='A1 foo') 317 | f2.translate(name='C3 foo', language='fr') 318 | f2.save() 319 | 320 | f3 = FooCategory.objects.create(name='C3 foo') 321 | f3.translate(name='A1 foo', language='fr') 322 | f3.save() 323 | 324 | qs_en = FooCategory.objects.all() 325 | self.assertEquals(qs_en[0], f2) 326 | self.assertEquals(qs_en[1], f1) 327 | self.assertEquals(qs_en[2], f3) 328 | 329 | translation.activate('fr') 330 | qs_fr = FooCategory.objects.all() 331 | self.assertEquals(qs_fr[0], f3) 332 | self.assertEquals(qs_fr[1], f1) 333 | self.assertEquals(qs_fr[2], f2) 334 | 335 | def testFilteringOnRelatedObjectsTransField(self): 336 | # Test filtering on related object's translatable field 337 | obj = Foo.objects.create(name='English Foo', price=10) 338 | obj.translate(name='French Foo', language='fr') 339 | obj.save() 340 | 341 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 342 | obj2.translate(name='Another French Foo', language='fr') 343 | obj2.save() 344 | 345 | m1 = FooRel.objects.create(myfoo=obj, desc="description 1") 346 | m2 = FooRel.objects.create(myfoo=obj2, desc="description 2") 347 | 348 | qs = FooRel.objects.filter(myfoo__name='Another English Foo') 349 | self.assertEquals(qs.count(), 1) 350 | self.assertEquals(qs[0], m2) 351 | 352 | qs = FooRel.objects.filter(myfoo__name__startswith='English') 353 | self.assertEquals(qs.count(), 1) 354 | self.assertEquals(qs[0], m1) 355 | 356 | translation.activate('fr') 357 | 358 | qs = FooRel.objects.filter(myfoo__name='Another French Foo') 359 | self.assertEquals(qs.count(), 1) 360 | self.assertEquals(qs[0], m2) 361 | 362 | qs = FooRel.objects.filter(myfoo__name__startswith='French') 363 | self.assertEquals(qs.count(), 1) 364 | self.assertEquals(qs[0], m1) 365 | 366 | def testFilteringOnRelatedObjectsUsingExplicitFieldName(self): 367 | obj = Foo.objects.create(name='English Foo', price=10) 368 | obj.translate(name='French Foo', language='fr') 369 | obj.save() 370 | 371 | obj2 = Foo.objects.create(name='Another English Foo', price=20) 372 | obj2.translate(name='Another French Foo', language='fr') 373 | obj2.save() 374 | 375 | m1 = FooRel.objects.create(myfoo=obj, desc="description 1") 376 | m2 = FooRel.objects.create(myfoo=obj2, desc="description 2") 377 | 378 | # we're in english 379 | translation.activate('en') 380 | qs = FooRel.objects.filter(myfoo__name_en='English Foo') 381 | self.assertEquals(qs.count(), 1) 382 | self.assertEquals(qs[0], m1) 383 | 384 | qs = FooRel.objects.filter(myfoo__name_en__startswith='Another') 385 | self.assertEquals(qs.count(), 1) 386 | self.assertEquals(qs[0], m2) 387 | 388 | # try using the french field name 389 | qs = FooRel.objects.filter(myfoo__name_fr='French Foo') 390 | self.assertEquals(qs.count(), 1) 391 | self.assertEquals(qs[0], m1) 392 | 393 | qs = FooRel.objects.filter(myfoo__name_fr__startswith='Another') 394 | self.assertEquals(qs.count(), 1) 395 | self.assertEquals(qs[0], m2) 396 | 397 | # now try in french 398 | translation.activate('fr') 399 | qs = FooRel.objects.filter(myfoo__name_en='English Foo') 400 | self.assertEquals(qs.count(), 1) 401 | self.assertEquals(qs[0], m1) 402 | 403 | qs = FooRel.objects.filter(myfoo__name_en__startswith='Another') 404 | self.assertEquals(qs.count(), 1) 405 | self.assertEquals(qs[0], m2) 406 | 407 | # try using the french field name 408 | qs = FooRel.objects.filter(myfoo__name_fr='French Foo') 409 | self.assertEquals(qs.count(), 1) 410 | self.assertEquals(qs[0], m1) 411 | 412 | qs = FooRel.objects.filter(myfoo__name_fr__startswith='Another') 413 | self.assertEquals(qs.count(), 1) 414 | self.assertEquals(qs[0], m2) 415 | 416 | def testModelWithTranslatableFileField(self): 417 | doc = Doc.objects.create(pdf='something.pdf') 418 | doc.translate(pdf='something-fr.pdf', language='fr') 419 | doc.save() 420 | 421 | translation.activate('en') 422 | self.assertEqual(Doc.objects.get().pdf.url, 'something.pdf') 423 | 424 | translation.activate('fr') 425 | self.assertEqual(Doc.objects.get().pdf.url, 'something-fr.pdf') 426 | 427 | def testModelWithAFieldCalledLanguageThatIsNotTranslatable(self): 428 | lan = Lan.objects.create(name='Test en', language='en') 429 | lan.translate(name='Test fr', language='fr') 430 | lan.save() 431 | 432 | translation.activate('en') 433 | self.assertEqual(Lan.objects.get().name, 'Test en') 434 | self.assertEqual(Lan.objects.get().language, 'en') 435 | 436 | translation.activate('fr') 437 | self.assertEqual(Lan.objects.get().name, 'Test fr') 438 | self.assertEqual(Lan.objects.get().language, 'en') 439 | 440 | 441 | class InheritanceTests(LinguoTests): 442 | 443 | def testOrderingOfFieldsWithinModel(self): 444 | expected = ['id', 'price', 'name', 'name_fr', 'foo_ptr', 'quantity', 445 | 'description', 'description_fr'] 446 | for i in range(len(Bar._meta.fields)): 447 | self.assertEqual(Bar._meta.fields[i].name, expected[i]) 448 | 449 | def testCreation(self): 450 | translation.activate('en') 451 | obj = Bar.objects.create(name='Bar', description='test', 452 | price=9, quantity=2) 453 | 454 | obj = Bar.objects.get(pk=obj.pk) 455 | self.assertEquals(obj.name, 'Bar') 456 | self.assertEquals(obj.description, 'test') 457 | self.assertEquals(obj.price, 9) 458 | self.assertEquals(obj.quantity, 2) 459 | 460 | translation.activate('fr') 461 | Bar.objects.create(name='FrenchBar', description='test in french', 462 | price=7, quantity=5) 463 | translation.activate('en') 464 | 465 | self.assertEquals(Bar.objects.count(), 2) 466 | 467 | def testTranslate(self): 468 | """ 469 | We should be able to create a translation of an object. 470 | """ 471 | obj = Bar.objects.create(name='Bar', description='test', 472 | price=9, quantity=2) 473 | obj.translate(name='BarFr', description='test FR', 474 | language='fr') 475 | obj.save() 476 | 477 | # Refresh from db 478 | obj = Bar.objects.get(pk=obj.pk) 479 | 480 | self.assertEquals(obj.name, 'Bar') 481 | self.assertEquals(obj.description, 'test') 482 | self.assertEquals(obj.price, 9) 483 | self.assertEquals(obj.quantity, 2) 484 | 485 | translation.activate('fr') 486 | self.assertEquals(obj.name, 'BarFr') 487 | self.assertEquals(obj.description, 'test FR') 488 | self.assertEquals(obj.price, 9) 489 | self.assertEquals(obj.quantity, 2) 490 | 491 | def testDelayedCreation(self): 492 | obj = Bar() 493 | obj.name = 'Bar' 494 | obj.description = 'Some desc' 495 | obj.price = 9 496 | obj.quantity = 2 497 | 498 | translation.activate('fr') 499 | obj.name = 'BarFr' 500 | obj.description = 'Some desc fr' 501 | obj.save() 502 | 503 | translation.activate('en') 504 | obj = Bar.objects.get(pk=obj.pk) 505 | self.assertEquals(obj.name, 'Bar') 506 | self.assertEquals(obj.description, 'Some desc') 507 | self.assertEquals(obj.price, 9) 508 | self.assertEquals(obj.quantity, 2) 509 | 510 | translation.activate('fr') 511 | self.assertEquals(obj.name, 'BarFr') 512 | self.assertEquals(obj.description, 'Some desc fr') 513 | self.assertEquals(obj.price, 9) 514 | self.assertEquals(obj.quantity, 2) 515 | 516 | def testSwitchingActiveLanguageSetValuesOnTranslatedFields(self): 517 | obj = Bar.objects.create(name='Bar', description='test', 518 | price=9, quantity=2) 519 | obj.translate(name='BarFr', description='test FR', 520 | language='fr') 521 | 522 | translation.activate('fr') 523 | self.assertEquals(obj.name, 'BarFr') 524 | obj.name = 'NewBarFr' 525 | 526 | translation.activate('en') 527 | self.assertEquals(obj.name, 'Bar') 528 | 529 | obj.save() 530 | 531 | # Refresh from db 532 | obj = Foo.objects.get(id=obj.id) 533 | self.assertEquals(obj.name, 'Bar') 534 | translation.activate('fr') 535 | self.assertEquals(obj.name, 'NewBarFr') 536 | 537 | def testCreateTranslationWithNewValueForNonTransField(self): 538 | """ 539 | That value of non-trans fields should be the same for all translations. 540 | """ 541 | 542 | obj = Bar.objects.create(name='Bar', description='test', 543 | price=9, quantity=2) 544 | obj.translate(name='BarFr', description='test FR', 545 | price=20, quantity=40, language='fr') 546 | 547 | translation.activate('fr') 548 | self.assertEquals(obj.name, 'BarFr') 549 | self.assertEquals(obj.description, 'test FR') 550 | self.assertEquals(obj.price, 20) 551 | self.assertEquals(obj.quantity, 40) 552 | 553 | translation.activate('en') 554 | self.assertEquals(obj.price, 20) 555 | self.assertEquals(obj.quantity, 40) 556 | # Ensure no other fields were changed 557 | self.assertEquals(obj.name, 'Bar') 558 | self.assertEquals(obj.description, 'test') 559 | 560 | def testQuerysetUpdate(self): 561 | obj = Bar.objects.create(name='Bar', description='test', 562 | price=9, quantity=2) 563 | obj.translate(name='BarFr', description='test FR', 564 | language='fr') 565 | obj.save() 566 | 567 | obj2 = Bar.objects.create(name='Bar2', description='bar desc', 568 | price=13, quantity=5) 569 | obj2.translate(name='Bar2Fr', description='test2 FR', 570 | language='fr') 571 | obj2.save() 572 | 573 | qs = Bar.objects.all() 574 | self.assertEquals(qs.count(), 2) 575 | qs.update(name='NewBar', quantity=99) 576 | 577 | # Refresh objects from db 578 | obj = Bar.objects.get(pk=obj.pk) 579 | obj2 = Bar.objects.get(pk=obj2.pk) 580 | 581 | self.assertEquals(obj.name, 'NewBar') 582 | self.assertEquals(obj.quantity, 99) 583 | self.assertEquals(obj2.name, 'NewBar') 584 | self.assertEquals(obj2.quantity, 99) 585 | 586 | translation.activate('fr') 587 | self.assertEquals(obj.name, 'BarFr') 588 | self.assertEquals(obj.quantity, 99) 589 | self.assertEquals(obj2.name, 'Bar2Fr') 590 | self.assertEquals(obj2.quantity, 99) 591 | 592 | def testQuerysetUpdateInOtherLanguageSetsValuesOnOtherLanguageOnly(self): 593 | obj = Bar.objects.create(name='Bar', description='test', 594 | price=9, quantity=2) 595 | obj.translate(name='BarFr', description='test FR', 596 | language='fr') 597 | obj.save() 598 | 599 | obj2 = Bar.objects.create(name='Bar2', description='bar desc', 600 | price=13, quantity=5) 601 | obj2.translate(name='Bar2Fr', description='test2 FR', 602 | language='fr') 603 | obj2.save() 604 | 605 | translation.activate('fr') 606 | qs = Bar.objects.all() 607 | self.assertEquals(qs.count(), 2) 608 | qs.update(name='NewBarFr', quantity=99) 609 | 610 | # Refresh objects from db 611 | obj = Bar.objects.get(pk=obj.pk) 612 | obj2 = Bar.objects.get(pk=obj2.pk) 613 | 614 | self.assertEquals(obj.name, 'NewBarFr') 615 | self.assertEquals(obj.quantity, 99) 616 | self.assertEquals(obj2.name, 'NewBarFr') 617 | self.assertEquals(obj2.quantity, 99) 618 | 619 | translation.activate('en') 620 | self.assertEquals(obj.name, 'Bar') 621 | self.assertEquals(obj.quantity, 99) 622 | self.assertEquals(obj2.name, 'Bar2') 623 | self.assertEquals(obj2.quantity, 99) 624 | 625 | def testUniqueTogether(self): 626 | """ 627 | Ensure that the unique_together definitions in child is working. 628 | """ 629 | Moo.objects.create(name='Moo', price=3, q1=4) 630 | 631 | try: 632 | Moo.objects.create(name='Moo2', price=15, q1=4) 633 | except IntegrityError: 634 | pass 635 | else: 636 | self.fail() 637 | 638 | def testUniqueTogetherInParent(self): 639 | """ 640 | Ensure that the unique_together definitions in parent is working. 641 | """ 642 | Moo.objects.create(name='Moo', price=3, q1=4) 643 | try: 644 | Moo.objects.create(name='Moo', price=3, q1=88) 645 | except IntegrityError: 646 | pass 647 | else: 648 | self.fail() 649 | 650 | def testFilteringOnTransField(self): 651 | obj = Bar.objects.create(name='English Bar', description='English test', 652 | price=9, quantity=2) 653 | obj.translate(name='French Bar', description='French test', 654 | language='fr') 655 | obj.save() 656 | 657 | qs = Bar.objects.filter(name="English Bar") 658 | self.assertEquals(qs.count(), 1) 659 | self.assertEquals(qs[0], obj) 660 | 661 | qs = Bar.objects.filter(name__startswith="English") 662 | self.assertEquals(qs.count(), 1) 663 | self.assertEquals(qs[0], obj) 664 | qs = Bar.objects.exclude(name__startswith="English") 665 | self.assertEquals(qs.count(), 0) 666 | 667 | translation.activate('fr') 668 | qs = Bar.objects.filter(name="French Bar") 669 | self.assertEquals(qs.count(), 1) 670 | self.assertEquals(qs[0], obj) 671 | 672 | qs = Bar.objects.filter(name__startswith="French") 673 | self.assertEquals(qs.count(), 1) 674 | self.assertEquals(qs[0], obj) 675 | qs = Bar.objects.exclude(name__startswith="French") 676 | self.assertEquals(qs.count(), 0) 677 | 678 | def testOrderingOnTransField(self): 679 | obj = Bar.objects.create(name='English Bar', description='English test', 680 | price=9, quantity=2) 681 | obj.translate(name='French Bar', description='French test', 682 | language='fr') 683 | obj.save() 684 | 685 | obj2 = Bar.objects.create(name='Another English Bar', description='another english test', 686 | price=22, quantity=25) 687 | obj2.translate(name='Another French Bar', description='another french test', 688 | language='fr') 689 | obj2.save() 690 | 691 | qs = Bar.objects.order_by('name') 692 | self.assertEquals(qs.count(), 2) 693 | self.assertEquals(qs[0], obj2) 694 | self.assertEquals(qs[1], obj) 695 | 696 | translation.activate('fr') 697 | 698 | qs = Bar.objects.order_by('name') 699 | self.assertEquals(qs.count(), 2) 700 | self.assertEquals(qs[0], obj2) 701 | self.assertEquals(qs[1], obj) 702 | self.assertEquals(qs[1].name, 'French Bar') 703 | 704 | def testDefaultOrderingIsTransAndInheritedTransField(self): 705 | """ 706 | Test a model that has an inherited trans field in the default ordering. 707 | """ 708 | o1 = Ord.objects.create(name='B2 test', price=1) 709 | o1.translate(name='B2 test F', price=1, language='fr') 710 | o1.save() 711 | 712 | o2 = Ord.objects.create(name='A1 test', price=2, last_name='Appleseed') 713 | o2.translate(name='C3 test F', price=2, last_name='Charlie', language='fr') 714 | o2.save() 715 | 716 | o2b = Ord.objects.create(name='A1 test', price=3, last_name='Zoltan') 717 | o2b.translate(name='C3 test F', price=3, last_name='Bobby', language='fr') 718 | o2b.save() 719 | 720 | o3 = Ord.objects.create(name='C3 foo', price=4) 721 | o3.translate(name='A1 test F', price=4, language='fr') 722 | o3.save() 723 | 724 | qs_en = Ord.objects.all() 725 | self.assertEquals(qs_en[0], o2) 726 | self.assertEquals(qs_en[1], o2b) 727 | self.assertEquals(qs_en[2], o1) 728 | self.assertEquals(qs_en[3], o3) 729 | 730 | translation.activate('fr') 731 | qs_fr = Ord.objects.all() 732 | self.assertEquals(qs_fr[0], o3) 733 | self.assertEquals(qs_fr[1], o1) 734 | self.assertEquals(qs_fr[2], o2b) 735 | self.assertEquals(qs_fr[3], o2) 736 | 737 | def testFilteringOnRelatedObjectsTransField(self): 738 | obj = Bar.objects.create(name='English Bar', description='English test', 739 | price=9, quantity=2) 740 | obj.translate(name='French Bar', description='French test', 741 | language='fr') 742 | obj.save() 743 | 744 | obj2 = Bar.objects.create(name='Another English Bar', description='another english test', 745 | price=22, quantity=25) 746 | obj2.translate(name='Another French Bar', description='another french test', 747 | language='fr') 748 | obj2.save() 749 | 750 | m1 = BarRel.objects.create(mybar=obj, desc="description 1") 751 | m2 = BarRel.objects.create(mybar=obj2, desc="description 2") 752 | 753 | qs = BarRel.objects.filter(mybar__name='Another English Bar') 754 | self.assertEquals(qs.count(), 1) 755 | self.assertEquals(qs[0], m2) 756 | 757 | qs = BarRel.objects.filter(mybar__name__startswith='English') 758 | self.assertEquals(qs.count(), 1) 759 | self.assertEquals(qs[0], m1) 760 | 761 | translation.activate('fr') 762 | 763 | qs = BarRel.objects.filter(mybar__name='Another French Bar') 764 | self.assertEquals(qs.count(), 1) 765 | self.assertEquals(qs[0], m2) 766 | 767 | qs = BarRel.objects.filter(mybar__name__startswith='French') 768 | self.assertEquals(qs.count(), 1) 769 | self.assertEquals(qs[0], m1) 770 | 771 | def testExtendingAbstract(self): 772 | """ 773 | Test a model that extends an abstract model an defines a new 774 | non trans field. 775 | """ 776 | obj = Moe.objects.create(name='test', description='test description', 777 | price=5, quantity=3 778 | ) 779 | obj.translate(language='fr', 780 | name='test-fr', description='test description fr' 781 | ) 782 | obj.save() 783 | 784 | obj = Moe.objects.get(pk=obj.pk) 785 | self.assertEquals(obj.name, 'test') 786 | self.assertEquals(obj.description, 'test description') 787 | self.assertEquals(obj.price, 5) 788 | self.assertEquals(obj.quantity, 3) 789 | 790 | Moe.objects.create(name='Other', description='test other', price=15, quantity=13) 791 | 792 | self.assertEquals(Moe.objects.count(), 2) 793 | 794 | translation.activate('fr') 795 | self.assertEquals(obj.name, 'test-fr') 796 | self.assertEquals(obj.description, 'test description fr') 797 | self.assertEquals(obj.price, 5) 798 | self.assertEquals(obj.quantity, 3) 799 | 800 | def testExtendingAbstractKeepsNonTransFields(self): 801 | obj = Moe.objects.create( 802 | name='test', description='test description', price=5, quantity=3 803 | ) 804 | obj.translate(language='fr', 805 | name='test-fr', description='test description fr', 806 | price=13 # Changing price 807 | ) 808 | obj.quantity = 99 # Changing quantity 809 | obj.save() 810 | 811 | obj = Moe.objects.get(pk=obj.pk) 812 | self.assertEquals(obj.price, 13) 813 | self.assertEquals(obj.quantity, 99) 814 | 815 | obj.price = 66 816 | obj.quantity = 77 817 | obj.save() 818 | 819 | translation.activate('fr') 820 | self.assertEquals(obj.price, 66) 821 | self.assertEquals(obj.quantity, 77) 822 | 823 | 824 | class ForeignKeyTests(LinguoTests): 825 | 826 | def testModelWithFK(self): 827 | """ 828 | A trans model has a foreign key to another trans model. 829 | The foreign key is not language specific. 830 | """ 831 | obj = Foo.objects.create(name='English Foo', price=10) 832 | obj.translate(name='French Foo', language='fr') 833 | obj.save() 834 | 835 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 836 | obj2.translate(name='Another French Foo', language='fr') 837 | obj2.save() 838 | 839 | rel1 = Gem.objects.create(somefoo=obj, gemtype='a') 840 | rel1.translate(gemtype='b', language='fr') 841 | rel1.save() 842 | 843 | rel2 = Gem.objects.create(somefoo=obj2, gemtype='a') 844 | 845 | self.assertEquals(rel1.somefoo, obj) 846 | # Ensure the reverse manager returns expected results 847 | self.assertEquals(obj.gem_set.count(), 1) 848 | self.assertEquals(obj.gem_set.all()[0], rel1) 849 | 850 | translation.activate('fr') 851 | self.assertEquals(rel1.somefoo, obj) 852 | self.assertEquals(obj.gem_set.count(), 1) 853 | self.assertEquals(obj.gem_set.all()[0], rel1) 854 | 855 | translation.activate('en') 856 | self.assertEquals(rel2.somefoo, obj2) 857 | self.assertEquals(obj2.gem_set.count(), 1) 858 | self.assertEquals(obj2.gem_set.all()[0], rel2) 859 | 860 | def testChangeFKWithInTranslatedLanguage(self): 861 | obj = Foo.objects.create(name='English Foo', price=10) 862 | 863 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 864 | obj2.translate(name='Another French Foo', language='fr') 865 | obj2.save() 866 | 867 | rel1 = Gem.objects.create(somefoo=obj, gemtype='a') 868 | rel1.translate(gemtype='b', language='fr') 869 | rel1.save() 870 | 871 | translation.activate('fr') 872 | rel1.somefoo = obj2 873 | rel1.save() 874 | 875 | translation.activate('en') 876 | rel2 = Gem.objects.create(somefoo=obj2, gemtype='a') 877 | rel1 = Gem.objects.get(pk=rel1.pk) 878 | self.assertEquals(rel1.somefoo, obj2) 879 | self.assertEquals(rel2.somefoo, obj2) 880 | 881 | self.assertEquals(obj2.gem_set.count(), 2) 882 | self.assertEquals(obj2.gem_set.order_by('id')[0], rel1) 883 | self.assertEquals(obj2.gem_set.order_by('id')[1], rel2) 884 | 885 | translation.activate('fr') 886 | self.assertEquals(obj2.gem_set.count(), 2) 887 | self.assertEquals(obj2.gem_set.order_by('id')[0], rel1) 888 | self.assertEquals(obj2.gem_set.order_by('id')[1], rel2) 889 | 890 | self.assertEquals(obj.gem_set.count(), 0) 891 | 892 | def testRemoveFk(self): 893 | """ 894 | Test for consistency when you remove a foreign key connection. 895 | """ 896 | obj = Foo.objects.create(name='English Foo', price=10) 897 | obj.translate(name='French Foo', language='fr') 898 | 899 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 900 | 901 | rel1 = Gem.objects.create(somefoo=obj, gemtype='a') 902 | rel1.translate(gemtype='b', language='fr') 903 | rel1.somefoo = None 904 | rel1.save() 905 | 906 | rel2 = Gem.objects.create(somefoo=obj2, gemtype='a') 907 | 908 | translation.activate('fr') 909 | self.assertEquals(rel1.somefoo, None) 910 | self.assertEquals(rel2.somefoo, obj2) 911 | self.assertEquals(obj.gem_set.count(), 0) 912 | self.assertEquals(obj2.gem_set.count(), 1) 913 | 914 | def testFKReverseCreation(self): 915 | """ 916 | Test creating an object using the reverse manager. 917 | """ 918 | Foo.objects.create(name='English Foo', price=10) 919 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 920 | obj2.translate(name='Another French Foo', language='fr') 921 | obj2.save() 922 | 923 | rel1 = obj2.gem_set.create(gemtype='a') 924 | 925 | obj2 = Foo.objects.get(pk=obj2.pk) 926 | 927 | self.assertEquals(obj2.gem_set.count(), 1) 928 | self.assertEquals(obj2.gem_set.order_by('id')[0], rel1) 929 | 930 | translation.activate('fr') 931 | self.assertEquals(obj2.gem_set.count(), 1) 932 | self.assertEquals(obj2.gem_set.order_by('id')[0], rel1) 933 | 934 | def testFKReverseAddition(self): 935 | """ 936 | Test adding an object using the reverse manager. 937 | """ 938 | obj = Foo.objects.create(name='English Foo', price=10) 939 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 940 | obj2.translate(name='Another French Foo', language='fr') 941 | obj2.save() 942 | 943 | rel1 = Gem.objects.create(somefoo=obj, gemtype='a') 944 | obj2.gem_set.add(rel1) 945 | rel1 = Gem.objects.get(pk=rel1.pk) 946 | self.assertEquals(rel1.somefoo, obj2) 947 | self.assertEquals(obj2.gem_set.count(), 1) 948 | self.assertEquals(obj2.gem_set.all()[0], rel1) 949 | 950 | def testFKReverseRemoval(self): 951 | """ 952 | Test removing an object using the reverse manager. 953 | """ 954 | obj = Foo.objects.create(name='English Foo', price=10) 955 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 956 | obj2.translate(name='Another French Foo', language='fr') 957 | obj2.save() 958 | 959 | rel1 = Gem.objects.create(somefoo=obj, gemtype='a') 960 | rel1.translate(gemtype='b', language='fr') 961 | 962 | obj2.gem_set.add(rel1) 963 | 964 | rel1 = Gem.objects.get(pk=rel1.pk) 965 | self.assertEquals(rel1.somefoo, obj2) 966 | 967 | self.assertEquals(obj.gem_set.count(), 0) 968 | self.assertEquals(obj2.gem_set.count(), 1) 969 | self.assertEquals(obj2.gem_set.all()[0], rel1) 970 | 971 | translation.activate('fr') 972 | self.assertEquals(rel1.somefoo, obj2) 973 | self.assertEquals(obj.gem_set.count(), 0) 974 | self.assertEquals(obj2.gem_set.count(), 1) 975 | self.assertEquals(obj2.gem_set.all()[0], rel1) 976 | 977 | translation.activate('en') 978 | obj2.gem_set.remove(rel1) 979 | rel1 = Gem.objects.get(pk=rel1.pk) 980 | self.assertEquals(rel1.somefoo, None) 981 | self.assertEquals(obj2.gem_set.count(), 0) 982 | 983 | translation.activate('fr') 984 | self.assertEquals(rel1.somefoo, None) 985 | self.assertEquals(obj2.gem_set.count(), 0) 986 | 987 | def testSetFKInTranslatedLanguage(self): 988 | obj = Foo.objects.create(name='English Foo', price=10) 989 | obj.translate(name='French Foo', language='fr') 990 | obj.save() 991 | 992 | translation.activate('fr') 993 | rel1 = Gem.objects.create(gemtype='a', somefoo=obj) 994 | 995 | translation.activate('en') 996 | self.assertEquals(obj.gem_set.count(), 1) 997 | self.assertEquals(obj.gem_set.all()[0], rel1) 998 | 999 | def testFKReverseAdditionOnTranslatedLanguage(self): 1000 | obj = Foo.objects.create(name='English Foo', price=10) 1001 | obj.translate(name='French Foo', language='fr') 1002 | obj.save() 1003 | 1004 | rel1 = Gem.objects.create(gemtype='a') 1005 | 1006 | translation.activate('fr') 1007 | obj.gem_set.add(rel1) 1008 | rel1 = Gem.objects.get(pk=rel1.pk) 1009 | 1010 | self.assertEquals(rel1.somefoo, obj) 1011 | 1012 | self.assertEquals(obj.gem_set.count(), 1) 1013 | self.assertEquals(obj.gem_set.all()[0], rel1) 1014 | 1015 | 1016 | class ManyToManyTests(LinguoTests): 1017 | 1018 | def testCreateM2M(self): 1019 | obj = Foo.objects.create(name='English Foo', price=10) 1020 | cat = obj.categories.create(name='C1') 1021 | cat2 = FooCategory.objects.create(name='C2') 1022 | 1023 | self.assertEquals(obj.categories.count(), 1) 1024 | self.assertEquals(obj.categories.all()[0], cat) 1025 | 1026 | obj.translate(language='fr', name='French Foo') 1027 | obj.save() 1028 | 1029 | translation.activate('fr') 1030 | self.assertEquals(obj.categories.count(), 1) 1031 | self.assertEquals(obj.categories.all()[0], cat) 1032 | 1033 | # Reverse lookup should return only foo 1034 | self.assertEquals(cat.foo_set.count(), 1) 1035 | self.assertEquals(cat.foo_set.all()[0], obj) 1036 | 1037 | translation.activate('en') 1038 | cat.translate(language='fr', name='C1 fr') 1039 | cat.save() 1040 | 1041 | translation.activate('fr') 1042 | self.assertEquals(cat.foo_set.all()[0], obj) 1043 | 1044 | translation.activate('en') 1045 | obj2 = Foo.objects.create(name='Another Foo', price=5) 1046 | self.assertEquals(obj2.categories.count(), 0) 1047 | 1048 | self.assertEquals(cat.foo_set.count(), 1) 1049 | self.assertEquals(cat.foo_set.all()[0], obj) 1050 | self.assertEquals(cat2.foo_set.count(), 0) 1051 | 1052 | def testRemovingM2M(self): 1053 | obj = Foo.objects.create(name='English Foo', price=10) 1054 | obj.translate(language='fr', name='French Foo') 1055 | obj.save() 1056 | 1057 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 1058 | 1059 | cat = obj.categories.create(name='C1') 1060 | cat2 = obj2.categories.create(name='C2') 1061 | translation.activate('fr') 1062 | cat3 = obj.categories.create(name='C3') 1063 | translation.activate('en') 1064 | 1065 | self.assertEquals(obj.categories.count(), 2) 1066 | 1067 | translation.activate('fr') 1068 | self.assertEquals(obj.categories.count(), 2) 1069 | obj.categories.remove(cat) 1070 | self.assertEquals(obj.categories.count(), 1) 1071 | self.assertEquals(obj.categories.all()[0], cat3) 1072 | 1073 | translation.activate('en') 1074 | self.assertEquals(obj.categories.count(), 1) 1075 | self.assertEquals(obj.categories.all()[0], cat3) 1076 | 1077 | self.assertEquals(obj2.categories.count(), 1) 1078 | self.assertEquals(obj2.categories.all()[0], cat2) 1079 | self.assertEquals(cat2.foo_set.all()[0], obj2) 1080 | 1081 | def testClearingM2M(self): 1082 | obj = Foo.objects.create(name='English Foo', price=10) 1083 | obj.translate(language='fr', name='French Foo') 1084 | obj.save() 1085 | obj2 = Foo.objects.create(name='Another English Foo', price=12) 1086 | obj2.save() 1087 | 1088 | obj.categories.create(name='C1') 1089 | cat2 = obj2.categories.create(name='C2') 1090 | 1091 | translation.activate('fr') 1092 | obj.categories.create(name='C3') 1093 | 1094 | self.assertEquals(obj.categories.count(), 2) 1095 | 1096 | translation.activate('fr') 1097 | self.assertEquals(obj.categories.count(), 2) 1098 | obj.categories.clear() 1099 | self.assertEquals(obj.categories.count(), 0) 1100 | 1101 | translation.activate('en') 1102 | self.assertEquals(obj.categories.count(), 0) 1103 | 1104 | self.assertEquals(obj2.categories.count(), 1) 1105 | self.assertEquals(obj2.categories.all()[0], cat2) 1106 | self.assertEquals(cat2.foo_set.all()[0], obj2) 1107 | 1108 | 1109 | class FormTests(LinguoTests): 1110 | 1111 | def testModelForm(self): 1112 | form = BarForm() 1113 | self.assertEqual(len(form.fields), 7) 1114 | self.assertTrue('name' in form.fields) 1115 | self.assertTrue('name_fr' in form.fields) 1116 | self.assertTrue('price' in form.fields) 1117 | self.assertTrue('categories' in form.fields) 1118 | self.assertTrue('quantity' in form.fields) 1119 | self.assertTrue('description' in form.fields) 1120 | self.assertTrue('description_fr' in form.fields) 1121 | 1122 | data = {'name': 'Test', 'name_fr': 'French Test', 'price': 13, 1123 | 'quantity': 3, 'description': 'This is a test', 'description_fr': 'French Description', 1124 | } 1125 | form = BarForm(data=data) 1126 | self.assertEqual(unicode(form['name'].label), u'Name') 1127 | self.assertEqual(unicode(form['name_fr'].label), u'Name (French)') 1128 | self.assertEqual(unicode(form['description'].label), u'Description') 1129 | self.assertEqual(unicode(form['description_fr'].label), u'Description (French)') 1130 | bar = form.save() 1131 | self.assertEqual(bar.name, 'Test') 1132 | self.assertEqual(bar.price, 13) 1133 | self.assertEqual(bar.quantity, 3) 1134 | self.assertEqual(bar.description, 'This is a test') 1135 | 1136 | translation.activate('fr') 1137 | self.assertEqual(bar.name, 'French Test') 1138 | self.assertEqual(bar.price, 13) 1139 | self.assertEqual(bar.quantity, 3) 1140 | self.assertEqual(bar.description, 'French Description') 1141 | 1142 | translation.activate('en') 1143 | # Create the form with an instance 1144 | data2 = {'name': 'Changed', 'name_fr': 'Changed French', 'price': 43, 1145 | 'quantity': 22, 'description': 'Changed description', 1146 | 'description_fr': 'Changed description French' 1147 | } 1148 | form = BarForm(instance=bar, data=data2) 1149 | bar = form.save() 1150 | self.assertEqual(bar.name, 'Changed') 1151 | self.assertEqual(bar.price, 43) 1152 | self.assertEqual(bar.quantity, 22) 1153 | self.assertEqual(bar.description, 'Changed description') 1154 | 1155 | translation.activate('fr') 1156 | self.assertEqual(bar.name, 'Changed French') 1157 | self.assertEqual(bar.price, 43) 1158 | self.assertEqual(bar.quantity, 22) 1159 | self.assertEqual(bar.description, 'Changed description French') 1160 | 1161 | def testModelFormInSecondaryLanguage(self): 1162 | translation.activate('fr') 1163 | form = BarForm() 1164 | # When we are in French name and description point to French fields (not the English) 1165 | # name_fr and description_fr are actually redundant 1166 | # But we want name_fr and description_fr to take precedence over name and description 1167 | data = {'name': 'Test', 'name_fr': 'French Test', 'price': 13, 1168 | 'quantity': 3, 'description': 'This is a test', 'description_fr': 'French Description', 1169 | } 1170 | form = BarForm(data=data) 1171 | 1172 | # These translations are not meant to be correct it is solely for the purpose of testing 1173 | self.assertEqual(unicode(form['name'].label), u'Neom') 1174 | self.assertEqual(unicode(form['name_fr'].label), u'Neom (Français)') 1175 | self.assertEqual(unicode(form['description'].label), u'Description') # This does not get translated because Django generates the verbose_name as a string 1176 | self.assertEqual(unicode(form['description_fr'].label), u'Déscriptione (Français)') 1177 | bar = form.save() 1178 | 1179 | translation.activate('en') 1180 | self.assertEqual(bar.name, '') 1181 | self.assertEqual(bar.price, 13) 1182 | self.assertEqual(bar.quantity, 3) 1183 | self.assertEqual(bar.description, '') 1184 | 1185 | translation.activate('fr') 1186 | self.assertEqual(bar.name, 'French Test') 1187 | self.assertEqual(bar.price, 13) 1188 | self.assertEqual(bar.quantity, 3) 1189 | self.assertEqual(bar.description, 'French Description') 1190 | 1191 | def testModelFormWithFieldsSpecified(self): 1192 | form = BarFormWithFieldsSpecified() 1193 | self.assertEqual(len(form.fields), 4) 1194 | self.assertTrue('name' in form.fields) 1195 | self.assertTrue('price' in form.fields) 1196 | self.assertTrue('quantity' in form.fields) 1197 | self.assertTrue('description' in form.fields) 1198 | 1199 | data = {'name': 'Test', 'price': 13, 1200 | 'quantity': 3, 'description': 'This is a test', 1201 | } 1202 | form = BarFormWithFieldsSpecified(data=data) 1203 | bar = form.save() 1204 | self.assertEqual(bar.name, 'Test') 1205 | self.assertEqual(bar.price, 13) 1206 | self.assertEqual(bar.quantity, 3) 1207 | self.assertEqual(bar.description, 'This is a test') 1208 | 1209 | translation.activate('fr') 1210 | self.assertEqual(bar.name, '') 1211 | self.assertEqual(bar.price, 13) 1212 | self.assertEqual(bar.quantity, 3) 1213 | self.assertEqual(bar.description, '') 1214 | 1215 | translation.activate('en') 1216 | # Create the form with an instance 1217 | data2 = {'name': 'Changed', 'price': 43, 1218 | 'quantity': 22, 'description': 'Changed description', 1219 | } 1220 | form = BarFormWithFieldsSpecified(instance=bar, data=data2) 1221 | bar = form.save() 1222 | self.assertEqual(bar.name, 'Changed') 1223 | self.assertEqual(bar.price, 43) 1224 | self.assertEqual(bar.quantity, 22) 1225 | self.assertEqual(bar.description, 'Changed description') 1226 | 1227 | translation.activate('fr') 1228 | self.assertEqual(bar.name, '') 1229 | self.assertEqual(bar.price, 43) 1230 | self.assertEqual(bar.quantity, 22) 1231 | self.assertEqual(bar.description, '') 1232 | 1233 | def testModelFormWithFieldsSpecifiedInSecondaryLanguage(self): 1234 | translation.activate('fr') 1235 | form = BarFormWithFieldsSpecified() 1236 | self.assertEqual(len(form.fields), 4) 1237 | self.assertTrue('name' in form.fields) 1238 | self.assertTrue('price' in form.fields) 1239 | self.assertTrue('quantity' in form.fields) 1240 | self.assertTrue('description' in form.fields) 1241 | 1242 | data = {'name': 'Test French', 'price': 13, 1243 | 'quantity': 3, 'description': 'This is a French test', 1244 | } 1245 | form = BarFormWithFieldsSpecified(data=data) 1246 | bar = form.save() 1247 | self.assertEqual(bar.name, 'Test French') 1248 | self.assertEqual(bar.price, 13) 1249 | self.assertEqual(bar.quantity, 3) 1250 | self.assertEqual(bar.description, 'This is a French test') 1251 | 1252 | translation.activate('en') 1253 | self.assertEqual(bar.name, '') 1254 | self.assertEqual(bar.price, 13) 1255 | self.assertEqual(bar.quantity, 3) 1256 | self.assertEqual(bar.description, '') 1257 | 1258 | translation.activate('fr') 1259 | # Create the form with an instance 1260 | data2 = {'name': 'Changed', 'price': 43, 1261 | 'quantity': 22, 'description': 'Changed description', 1262 | } 1263 | form = BarFormWithFieldsSpecified(instance=bar, data=data2) 1264 | bar = form.save() 1265 | self.assertEqual(bar.name, 'Changed') 1266 | self.assertEqual(bar.price, 43) 1267 | self.assertEqual(bar.quantity, 22) 1268 | self.assertEqual(bar.description, 'Changed description') 1269 | 1270 | translation.activate('en') 1271 | self.assertEqual(bar.name, '') 1272 | self.assertEqual(bar.price, 43) 1273 | self.assertEqual(bar.quantity, 22) 1274 | self.assertEqual(bar.description, '') 1275 | 1276 | 1277 | if django.VERSION[:3] >= (1, 1, 2): # The AdminTests only pass with django >= 1.1.2 (but compatibility is django >= 1.0.3) 1278 | class AdminTests(LinguoTests): 1279 | 1280 | def setUp(self): 1281 | super(AdminTests, self).setUp() 1282 | 1283 | self.user = User.objects.create_user(username='test', password='test', 1284 | email='test@test.com' 1285 | ) 1286 | self.user.is_staff = True 1287 | self.user.is_superuser = True 1288 | self.user.save() 1289 | self.client.login(username='test', password='test') 1290 | 1291 | def testAdminChangelistFeatures(self): 1292 | # Create some Bar objects 1293 | b1 = Bar.objects.create(name="apple", price=2, description="hello world", quantity=1) 1294 | b1.translate(name="pomme", description="allo monde", language="fr") 1295 | b1.save() 1296 | 1297 | b2 = Bar.objects.create(name="computer", price=3, description="oh my god", quantity=3) 1298 | b2.translate(name="ordinator", description="oh mon dieu", language="fr") 1299 | b2.save() 1300 | 1301 | url = reverse('admin:tests_bar_changelist') 1302 | response = self.client.get(url) 1303 | 1304 | # Check that the correct language is being displayed 1305 | self.assertContains(response, 'hello world') 1306 | self.assertContains(response, 'oh my god') 1307 | 1308 | # Check the list filters 1309 | self.assertContains(response, '?name=apple') 1310 | self.assertContains(response, '?name=computer') 1311 | 1312 | # Check that the filtering works 1313 | response = self.client.get(url, {'name': 'computer'}) 1314 | self.assertContains(response, 'oh my god') 1315 | self.assertNotContains(response, 'hello world') 1316 | 1317 | # Check the searching 1318 | response = self.client.get(url, {'q': 'world'}) 1319 | self.assertContains(response, 'hello world') 1320 | self.assertNotContains(response, 'oh my god') 1321 | 1322 | def testAdminAddSubmission(self): 1323 | url = reverse('admin:tests_bar_add') 1324 | response = self.client.post(url, data={ 1325 | 'name': 'Bar', 1326 | 'name_fr': 'French Bar', 1327 | 'price': 12, 1328 | 'quantity': 5, 1329 | 'description': 'English description.', 1330 | 'description_fr': 'French description.' 1331 | }) 1332 | self.assertEqual(response.status_code, 302) 1333 | 1334 | def testAdminChangeSubmission(self): 1335 | obj = Bar(name='Bar', price=12, quantity=5, description='Hello') 1336 | obj.translate(language='fr', name='French Bar', description='French Hello') 1337 | obj.save() 1338 | 1339 | url = reverse('admin:tests_bar_change', args=[obj.id]) 1340 | response = self.client.post(url, data={ 1341 | 'name': 'Bar2', 1342 | 'name_fr': 'French Bar2', 1343 | 'price': 222, 1344 | 'quantity': 55, 1345 | 'description': 'Hello2', 1346 | 'description_fr': 'French Hello2' 1347 | }) 1348 | self.assertEqual(response.status_code, 302) 1349 | 1350 | 1351 | class TestMultilingualForm(LinguoTests): 1352 | def testCreatesModelInstanceWithAllFieldValues(self): 1353 | translation.activate('fr') 1354 | form = MultilingualBarFormAllFields(data={ 1355 | 'name': 'Bar', 1356 | 'name_fr': 'French Bar', 1357 | 'price': 12, 1358 | 'quantity': 5, 1359 | 'description': 'English description.', 1360 | 'description_fr': 'French description.' 1361 | }) 1362 | 1363 | instance = form.save() 1364 | 1365 | translation.activate('en') 1366 | instance = Bar.objects.get(id=instance.id) # Refresh from db 1367 | 1368 | self.assertEqual(instance.name, 'Bar') 1369 | self.assertEqual(instance.price, 12) 1370 | self.assertEqual(instance.quantity, 5) 1371 | self.assertEqual(instance.description, 'English description.') 1372 | 1373 | translation.activate('fr') 1374 | self.assertEqual(instance.name, 'French Bar') 1375 | self.assertEqual(instance.price, 12) 1376 | self.assertEqual(instance.quantity, 5) 1377 | self.assertEqual(instance.description, 'French description.') 1378 | 1379 | def testUpdatesModelInstanceWithAllFieldValues(self): 1380 | instance = Bar(name='Bar', price=12, quantity=5, description='Hello') 1381 | instance.translate(language='fr', name='French Bar', description='French Hello') 1382 | instance.save() 1383 | 1384 | translation.activate('fr') 1385 | form = MultilingualBarFormAllFields(instance=instance, data={ 1386 | 'name': 'Bar2', 1387 | 'name_fr': 'French Bar2', 1388 | 'price': 222, 1389 | 'quantity': 55, 1390 | 'description': 'Hello2', 1391 | 'description_fr': 'French Hello2' 1392 | }) 1393 | 1394 | instance = form.save() 1395 | 1396 | translation.activate('en') 1397 | instance = Bar.objects.get(id=instance.id) # Refresh from db 1398 | 1399 | self.assertEqual(instance.name, 'Bar2') 1400 | self.assertEqual(instance.price, 222) 1401 | self.assertEqual(instance.quantity, 55) 1402 | self.assertEqual(instance.description, 'Hello2') 1403 | 1404 | translation.activate('fr') 1405 | self.assertEqual(instance.name, 'French Bar2') 1406 | self.assertEqual(instance.price, 222) 1407 | self.assertEqual(instance.quantity, 55) 1408 | self.assertEqual(instance.description, 'French Hello2') 1409 | 1410 | def testInitialDataContainsAllFieldValues(self): 1411 | instance = Bar(name='Bar', price=12, quantity=5, description='Hello') 1412 | instance.translate(language='fr', name='French Bar', description='French Hello') 1413 | instance.save() 1414 | 1415 | translation.activate('fr') 1416 | form = MultilingualBarFormAllFields(instance=instance) 1417 | self.assertEqual(form.initial['name'], 'Bar') 1418 | self.assertEqual(form.initial['name_fr'], 'French Bar') 1419 | self.assertEqual(form.initial['price'], 12) 1420 | self.assertEqual(form.initial['quantity'], 5) 1421 | self.assertEqual(form.initial['description'], 'Hello') 1422 | self.assertEqual(form.initial['description_fr'], 'French Hello') 1423 | 1424 | 1425 | class TestsForUnupportedFeatures(object): # LinguoTests): 1426 | 1427 | def testTransFieldHasNotNullConstraint(self): 1428 | """ 1429 | Test a trans model with a trans field that has a not null constraint. 1430 | """ 1431 | pass 1432 | 1433 | def testExtendingToMakeTranslatable(self): 1434 | """ 1435 | Test the ability to extend a non-translatable model with MultilingualModel 1436 | in order to make some field translatable. 1437 | """ 1438 | pass 1439 | 1440 | def testSubclassingAbstractModelIntoTranslatableModel(self): 1441 | """ 1442 | Test the ability to subclass a a non-translatable Abstract model 1443 | and extend with MultilingualModel in order to make some field translatable. 1444 | """ 1445 | pass 1446 | 1447 | def testModelFormWithFieldsExcluded(self): 1448 | form = BarFormWithFieldsExcluded() 1449 | self.assertEqual(len(form.fields), 4) 1450 | self.assertTrue('price' in form.fields) 1451 | self.assertTrue('quantity' in form.fields) 1452 | self.assertTrue('description' in form.fields) 1453 | self.assertTrue('description_fr' in form.fields) 1454 | 1455 | def testAdminChangelistFeaturesInSecondaryLanguage(self): 1456 | 1457 | # Create some Bar objects 1458 | b1 = Bar.objects.create(name="apple", price=2, description="hello world", quantity=1) 1459 | b1.translate(name="pomme", description="allo monde", language="fr") 1460 | b1.save() 1461 | 1462 | b2 = Bar.objects.create(name="computer", price=3, description="oh my god", quantity=3) 1463 | b2.translate(name="ordinator", description="oh mon dieu", language="fr") 1464 | b2.save() 1465 | 1466 | translation.activate('fr') 1467 | url = reverse('admin:tests_bar_changelist') 1468 | response = self.client.get(url) 1469 | 1470 | # Check that the correct language is being displayed 1471 | self.assertContains(response, 'allo monde') 1472 | self.assertContains(response, 'oh mon dieu') 1473 | self.assertNotContains(response, 'hello world') 1474 | self.assertNotContains(response, 'oh my god') 1475 | 1476 | # Check the list filters 1477 | self.assertContains(response, '?name=pomme') 1478 | self.assertContains(response, '?name=ordinator') 1479 | 1480 | # Check that the filtering works 1481 | response = self.client.get(url, {'name': 'ordinator'}) 1482 | self.assertContains(response, 'oh mon dieu') 1483 | self.assertNotContains(response, 'allo monde') 1484 | 1485 | # Check the searching 1486 | response = self.client.get(url, {'q': 'monde'}) 1487 | self.assertContains(response, 'allo monde') 1488 | self.assertNotContains(response, 'oh mon dieu') 1489 | -------------------------------------------------------------------------------- /linguo/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | 3 | from django.contrib import admin 4 | 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | url(r'^admin/', include(admin.site.urls)), 10 | ) 11 | -------------------------------------------------------------------------------- /linguo/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils import translation 3 | 4 | 5 | def get_real_field_name(field_name, language): 6 | """ 7 | Returns the field that stores the value of the given translation 8 | in the specified language. 9 | """ 10 | lang_code = get_normalized_language(language) 11 | if lang_code == get_normalized_language(settings.LANGUAGES[0][0]): 12 | return field_name 13 | else: 14 | return '%s_%s' % (field_name, language) 15 | 16 | 17 | def get_normalized_language(language_code): 18 | """ 19 | Returns the actual language extracted from the given language code 20 | (ie. locale stripped off). For example, 'en-us' becomes 'en'. 21 | """ 22 | return language_code.split('-')[0] 23 | 24 | 25 | def get_current_language(): 26 | """ 27 | Wrapper around `translation.get_language` that returns the normalized 28 | language code. 29 | """ 30 | return get_normalized_language(translation.get_language()) 31 | -------------------------------------------------------------------------------- /linguo/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | import linguo 4 | 5 | 6 | setup( 7 | name='django-linguo', 8 | packages=['linguo', 'linguo.tests'], 9 | package_data={'linguo': ['tests/locale/*/LC_MESSAGES/*']}, 10 | version=linguo.__version__, 11 | description=linguo.__doc__, 12 | long_description=open('README.rst').read(), 13 | classifiers=[ 14 | 'Framework :: Django', 15 | 'Intended Audience :: Developers', 16 | 'License :: OSI Approved :: BSD License', 17 | 'Operating System :: OS Independent', 18 | 'Topic :: Software Development' 19 | ], 20 | author='Zach Mathew', 21 | url='http://github.com/zmathew/django-linguo', 22 | license='BSD', 23 | ) 24 | --------------------------------------------------------------------------------