diff --git a/.travis.yml b/.travis.yml index 95cf965f..d59a014f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,54 +1,81 @@ language: python python: - - "2.6" - "2.7" - - "3.2" - "3.3" + - "3.4" + - "3.5" + - "3.6" env: - - DJANGO=1.4 DB=sqlite - - DJANGO=1.4 DB=postgres - - DJANGO=1.4 DB=mysql - - DJANGO=1.5 DB=sqlite - - DJANGO=1.5 DB=postgres - - DJANGO=1.5 DB=mysql - - DJANGO=1.6 DB=sqlite - - DJANGO=1.6 DB=postgres - - DJANGO=1.6 DB=mysql + - DJANGO=1.8 DB=sqlite + - DJANGO=1.8 DB=postgres + - DJANGO=1.8 DB=mysql + - DJANGO=1.9 DB=sqlite + - DJANGO=1.9 DB=postgres + - DJANGO=1.9 DB=mysql + - DJANGO=1.10 DB=sqlite + - DJANGO=1.10 DB=postgres + - DJANGO=1.10 DB=mysql + - DJANGO=1.11 DB=sqlite + - DJANGO=1.11 DB=postgres + - DJANGO=1.11 DB=mysql matrix: exclude: - - python: "3.2" - env: DJANGO=1.4 DB=sqlite - - python: "3.2" - env: DJANGO=1.4 DB=postgres - - python: "3.2" - env: DJANGO=1.4 DB=mysql + - python: "3.6" + env: DJANGO=1.8 DB=sqlite + - python: "3.6" + env: DJANGO=1.8 DB=mysql + - python: "3.6" + env: DJANGO=1.8 DB=postgres + + - python: "3.3" + env: DJANGO=1.9 DB=sqlite + - python: "3.3" + env: DJANGO=1.9 DB=mysql - python: "3.3" - env: DJANGO=1.4 DB=sqlite + env: DJANGO=1.9 DB=postgres + - python: "3.6" + env: DJANGO=1.9 DB=sqlite + - python: "3.6" + env: DJANGO=1.9 DB=mysql + - python: "3.6" + env: DJANGO=1.9 DB=postgres + + - python: "3.3" + env: DJANGO=1.10 DB=sqlite - python: "3.3" - env: DJANGO=1.4 DB=postgres + env: DJANGO=1.10 DB=postgres - python: "3.3" - env: DJANGO=1.4 DB=mysql + env: DJANGO=1.10 DB=mysql + - python: "3.6" + env: DJANGO=1.10 DB=sqlite + - python: "3.6" + env: DJANGO=1.10 DB=postgres + - python: "3.6" + env: DJANGO=1.10 DB=mysql - - python: "3.2" - env: DJANGO=1.5 DB=mysql - python: "3.3" - env: DJANGO=1.5 DB=mysql - - python: "3.2" - env: DJANGO=1.6 DB=mysql + env: DJANGO=1.11 DB=sqlite + - python: "3.3" + env: DJANGO=1.11 DB=postgres - python: "3.3" - env: DJANGO=1.6 DB=mysql + env: DJANGO=1.11 DB=mysql +before_install: + - pip install -q 'flake8<3' + - PYFLAKES_NODOCTEST=1 flake8 modeltranslation before_script: - - PYFLAKES_NODOCTEST=1 flake8 --max-line-length=100 modeltranslation - mysql -e 'create database modeltranslation;' - psql -c 'create database modeltranslation;' -U postgres install: - - if [[ $DB == mysql ]]; then pip install -q mysql-python --use-mirrors; fi - - if [[ $DB == postgres ]]; then pip install -q psycopg2 --use-mirrors; fi - - pip install -q Pillow --use-mirrors - - pip install -q flake8 --use-mirrors + - PYTHON=`python -c 'import sys; version=sys.version_info[:3]; print("{0}.{1}".format(*version))'` + - if [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "2" ]]; then pip install -q mysql-python; elif [[ $DB == mysql ]] && [[ ${PYTHON:0:1} == "3" ]]; then pip install -q mysqlclient; fi + - if [[ $DB == postgres ]]; then pip install -q psycopg2; fi + - pip install -q Pillow - IDJANGO=$(./travis.py $DJANGO) - pip install -q $IDJANGO - - pip install -e . --use-mirrors + - pip install -e . + - pip install -q coveralls script: - django-admin.py --version - - ./runtests.py + - coverage run --source=modeltranslation ./runtests.py +after_success: + coveralls diff --git a/AUTHORS.rst b/AUTHORS.rst index 73db49ee..fc2415ec 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,6 +27,24 @@ Contributors * Konrad Wojas * Bas Peschier * Oleg Prans +* Francesc Arpí Roca +* Mathieu Leplatre +* Thom Wiggers +* Warnar Boekkooi +* Alex Marandon +* Fabio Caccamo +* Vladimir Sinitsin +* Luca Corti +* Morgan Aubert +* Mathias Ettinger +* Daniel Loeb +* Stephen McDonald +* Lukas Lundgren +* zenoamaro +* oliphunt +* Venelin Stoykov +* Stratos Moros +* Benjamin Toueg * And many more ... (if you miss your name here, please let us know!) .. _django-linguo: https://github.com/zmathew/django-linguo diff --git a/CHANGELOG.txt b/CHANGELOG.txt index eb1eb231..25dcafd6 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,211 @@ +v0.12.2 +======= +Date: 2017-01-26 + + FIXED: order_by with expression + (resolves issue #398, thanks Benjamin Toueg) + + +v0.12.1 +======= +Date: 2017-04-05 + + FIXED: Issue in loaddata management command in combination with Django 1.11. + (resolves issue #401) + + +v0.12 +===== +Date: 2016-09-20 + + ADDED: Support for Django 1.10. + (resolves issue #360, thanks Jacek Tomaszewski and Primož Kerin) + +CHANGED: Original field value became more unreliable and undetermined; + please make sure you're not using it anywhere. See + http://django-modeltranslation.readthedocs.io/en/latest/usage.html#the-state-of-the-original-field +CHANGED: Let register decorator return decorated class + (resolves issue #360, thanks spacediver) + + FIXED: Deferred classes signal connection. + (resolves issue #379, thanks Jacek Tomaszewski) + FIXED: values_list + annotate combo bug. + (resolves issue #374, thanks Jacek Tomaszewski) + FIXED: Several flake8 and travis related issues. + (resolves issues #363, thanks Matthias K) + + +v0.11 +===== +Date: 2016-01-31 + +Released without changes. + + +v0.11rc2 +======== +Date: 2015-12-15 + + FIXED: Custom manager in migrations. + (resolves issues #330, #339 and #350, thanks Jacek Tomaszewski) + + +v0.11rc1 +======== +Date: 2015-12-07 + + ADDED: Support for Django 1.9 + (resolves issue #349, thanks Jacek Tomaszewski) + + +v0.10.2 +======= +Date: 2015-10-27 + + FIXED: Proxy model inheritance for Django >=1.8 + (resolves issues #304, thanks Stratos Moros) + + +v0.10.1 +======= +Date: 2015-09-04 + + FIXED: FallbackValuesListQuerySet.iterator which broke ORM datetimes + (resolves issue #324, thanks Venelin Stoykov) + + +v0.10.0 +======= +Date: 2015-07-03 + + ADDED: CSS support for bi-directional languages to TranslationAdmin + using mt-bidi class. + (resolves issue #317, thanks oliphunt) + ADDED: A decorator to handle registration of models. + (resolves issue #318, thanks zenoamaro) + + FIXED: Handled annotation fields when using values_list. + (resolves issue #321, thanks Lukas Lundgren) + + +v0.9.1 +====== +Date: 2015-05-14 + + FIXED: Handled deprecation of _meta._fill_fields_cache() for Django 1.8 + in add_translation_fields. + (resolves issue #304, thanks Mathias Ettinger and Daniel Loeb) + FIXED: Handled deprecation of transaction.commit_unless_managed for + Django 1.8 in sync_translation_fields management command. + (resolves issue #310) + FIXED: Fixed translatable fields discovery with the new _meta API and + generic relations for Django 1.8. + (resolves issue #309, thanks Morgan Aubert) + + +v0.9 +==== +Date: 2015-04-16 + + ADDED: Support for Django 1.8 and the new meta API. + (resolves issue #299, thanks Luca Corti and Jacek Tomaszewski) + + +v0.8.1 +====== +Date: 2015-04-02 + + FIXED: Using a queryset with select related. + (resolves issue #298, thanks Vladimir Sinitsin) + FIXED: Added missing jquery browser plugin. + (resolves issue #270, thanks Fabio Caccamo) + FIXED: Deprecated imports with Django >= 1.7 + (resolves issue #283, thanks Alex Marandon) + + +v0.8 +==== +Date: 2014-10-06 + + FIXED: JavaScript scoping issue with two jQuery versions in tabbed + translation fields. + (resolves issue #267, + thanks Wojtek Ruszczewski) + + ADDED: Patch db_column of translation fields in migration files. + (resolves issue #264, + thanks Thom Wiggers and Jacek Tomaszewski) + ADDED: Fallback to values and values_list. + (resolves issue #258, + thanks Jacek Tomaszewski) + + +v0.8b2 +====== +Date: 2014-07-18 + + ADDED: Explicit support for Python 3.4 (should have already worked for + older versions that supported Python 3). + (resolves issue #254) + ADDED: Support for Django 1.7 migrations. + + FIXED: Dict iteration Exception under Python 3. + (resolves issue #256, + thanks Jacek Tomaszewski) + FIXED: Reduce usage under Python 3. + (thanks Jacek Tomaszewski) + FIXED: Support for AppConfigs in INSTALLED_APPS + (resolves issue #252, + thanks Warnar Boekkooi, Jacek Tomaszewski) + FIXED: Rewrite field names in select_related. Fix deffered models registry. + Rewrite spanned queries on all levels for defer/only. + (resolves issue #248, + thanks Jacek Tomaszewski) + + +v0.8b1 +====== +Date: 2014-06-22 + + ADDED: Detect custom get_queryset on managers. + (resolves issue #242, + thanks Jacek Tomaszewski) + ADDED: Support for Django 1.7 and the new app-loading refactor. + (resolves issue #237) + ADDED: Added required_languages TranslationOptions + (resolves issue #143) + + FIXED: Fixed sync_translation_fields to be compatible with PostgreSQL. + (resolves issue #247, + thanks Jacek Tomaszewski) + FIXED: Manager .values() with no fields specified behaves as expected. + (resolves issue #247) + FIXED: Fieldset headers are not capitalized when group_fieldsets is enabled. + (resolves issue #234, + thanks Jacek Tomaszewski) + FIXED: Exclude for nullable field manager rewriting. + (resolves issue #231, + thanks Jacek Tomaszewski) + FIXED: Use AVAILABLE_LANGUAGES in sync_translation_fields management + command to detect missing fields. + (resolves issue #227, + thanks Mathieu Leplatre) + FIXED: Take db_column into account while syncing fields + (resolves issue #225, + thanks Mathieu Leplatre) + +CHANGED: Moved to get_queryset, which resolves a deprecation warning. + (resolves issue #244, + thanks Thom Wiggers) +CHANGED: Considered iframes in tabbed_translation_fields.js to support + third party apps like django-summernote. + (resolves issue #229, + thanks Francesc Arpí Roca) +CHANGED: Removed the http protocol from jquery-ui url in admin Media class. + (resolves issue #224, + thanks Francesc Arpí Roca) + + v0.7.3 ====== Date: 2014-01-05 diff --git a/PKG-INFO b/PKG-INFO index 56638790..2ebc3eff 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: django-modeltranslation -Version: 0.7.3 +Version: 0.12.2 Summary: Translates Django models using a registration approach. Home-page: https://github.com/deschler/django-modeltranslation Author: Peter Eschler, diff --git a/README.rst b/README.rst index e2f57340..53b79dfc 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Modeltranslation ================ -The modeltranslation application can be used to translate dynamic content of +The modeltranslation application is used to translate dynamic content of existing Django models to an arbitrary number of languages without having to change the original model classes. It uses a registration approach (comparable to Django's admin app) to be able to add translations to existing or new @@ -14,18 +14,34 @@ may they use translations or not, and you never have to touch the original model class. -.. image:: https://travis-ci.org/deschler/django-modeltranslation.png?branch=master +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat-square :target: https://travis-ci.org/deschler/django-modeltranslation +.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat-square + :target: https://coveralls.io/r/deschler/django-modeltranslation + +.. image:: https://img.shields.io/pypi/v/django-modeltranslation.svg?style=flat-square + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Latest PyPI version + +.. image:: https://img.shields.io/pypi/pyversions/django-modeltranslation.svg?style=flat-square + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Supported Python versions + +.. image:: https://img.shields.io/pypi/dm/django-modeltranslation.svg?style=flat-square + :target: https://pypi.python.org/pypi/django-modeltranslation/ + :alt: Number of PyPI downloads + Features ======== -- Add translations without changing existing models -- Fast, because translation fields are stored in the same table -- Supports inherited models -- Django admin support -- Unlimited number of target languages +- Add translations without changing existing models or views +- Translation fields are stored in the same table (no expensive joins) +- Supports inherited models (abstract and multi-table inheritance) +- Handle more than just text fields +- Django admin integration +- Flexible fallbacks, auto-population and more! Project Home @@ -34,7 +50,7 @@ https://github.com/deschler/django-modeltranslation Documentation ------------- -https://django-modeltranslation.readthedocs.org/en/latest/ +https://django-modeltranslation.readthedocs.org/en/latest Mailing List ------------ diff --git a/docs/modeltranslation/admin.rst b/docs/modeltranslation/admin.rst index b7f6c4b6..99423b83 100644 --- a/docs/modeltranslation/admin.rst +++ b/docs/modeltranslation/admin.rst @@ -30,7 +30,7 @@ formfield_for_dbfield The ``TranslationBaseModelAdmin`` class, which ``TranslationAdmin`` and all inline related classes in modeltranslation derive from, implements a special -method which is ``def formfield_for_dbfield(self, db_field, **kwargs)``. This +method which is ``formfield_for_dbfield(self, db_field, **kwargs)``. This method does the following: 1. Copies the widget of the original field to each of its translation fields. @@ -78,7 +78,7 @@ TranslationAdmin in Combination with Other Admin Classes If there already exists a custom admin class for a translated model and you don't want or can't edit that class directly there is another solution. -Taken a (fictional) reusable blog app which defines a model ``Entry`` and a +Taken a reusable blog app which defines a model ``Entry`` and a corresponding admin class called ``EntryAdmin``. This app is not yours and you don't want to touch it at all. diff --git a/docs/modeltranslation/caveats.rst b/docs/modeltranslation/caveats.rst index 41d60bd6..2939e21e 100644 --- a/docs/modeltranslation/caveats.rst +++ b/docs/modeltranslation/caveats.rst @@ -24,8 +24,42 @@ Outside a view (or a template), i.e. in normal Python code, a call to the ``get_language`` function still returns a value, but it might not what you expect. Since no request is involved, Django's machinery for discovering the user's preferred language is not activated. For this reason modeltranslation -adds a thin wrapper around the function which guarantees that the returned -language is listed in the ``LANGUAGES`` setting. +adds a thin wrapper (``modeltranslation.utils.get_language``) around the function +which guarantees that the returned language is listed in the ``LANGUAGES`` setting. The unittests use the ``django.utils.translation.trans_real`` functions to activate and deactive a specific language outside a view function. + +Using in combination with ``django-audit-log`` +---------------------------------------------- + +``django-audit-log`` is a package that allows you to track changes to your +model instances (`documentation`_). As ``django-audit-log`` behind the scenes +automatically creates "shadow" models for your tracked models, you have to +remember to register these shadow models for translation as well as your +regular models. Here's an example: + +.. code:: python + + from modeltranslation.translator import register, TranslationOptions + + from my_app import models + + + @register(models.MyModel) + @register(models.MyModel.audit_log.model) + class MyModelTranslationOptions(TranslationOptions): + """Translation options for MyModel.""" + + fields = ( + 'text', + 'title', + ) + +If you forget to register the shadow models, you will get an error like: + +.. code:: + + TypeError: 'text_es' is an invalid keyword argument for this function + +.. _documentation: https://django-audit-log.readthedocs.io/ diff --git a/docs/modeltranslation/commands.rst b/docs/modeltranslation/commands.rst index fc1a06e5..fe76dec7 100644 --- a/docs/modeltranslation/commands.rst +++ b/docs/modeltranslation/commands.rst @@ -8,19 +8,19 @@ Management Commands The ``update_translation_fields`` Command ----------------------------------------- -In case the modeltranslation app was installed on an existing project and you +In case modeltranslation was installed in an existing project and you have specified to translate fields of models which are already synced to the database, you have to update your database schema (see :ref:`db-fields`). Unfortunately the newly added translation fields on the model will be empty then, and your templates will show the translated value of the fields (see -Rule 1) which will be empty in this case. To correctly initialize the -default translation field you can use the ``update_translation_fields`` +:ref:`Rule 1 `) which will be empty in this case. To correctly initialize +the default translation field you can use the ``update_translation_fields`` command: .. code-block:: console - $ ./manage.py update_translation_fields + $ python manage.py update_translation_fields Taken the news example used throughout the documentation this command will copy the value from the news object's ``title`` field to the default translation @@ -47,11 +47,12 @@ The ``sync_translation_fields`` Command .. code-block:: console - $ ./manage.py sync_translation_fields + $ python manage.py sync_translation_fields This command compares the database and translated models definitions (finding new translation fields) and provides SQL statements to alter tables. You should run this command after adding -new language or deciding to translate new field in a ``TranslationOptions``. +a new language to your ``settings.LANGUAGES`` or a new field to the ``TranslationOptions`` of +a registered model. However, if you are using South in your project, in most cases it's recommended to use migration instead of ``sync_translation_fields``. See :ref:`db-fields` for detailed info and use cases. @@ -62,28 +63,29 @@ The ``loaddata`` Command .. versionadded:: 0.7 -It is just extension to original ``loaddata`` command which adds an optional ``populate`` keyword. -If specified, then normal loading command will be run under selected auto-population modes. +An extended version of Django's original ``loaddata`` command which adds an optional +``populate`` keyword. If the keyword is specified, the normal loading command will be +run under the selected auto-population modes. By default no auto-population is performed. .. code-block:: console - $ ./manage.py loaddata --populate=all fixtures.json + $ python manage.py loaddata --populate=all fixtures.json Allowed modes are listed :ref:`here `. To choose ``False`` (turn off auto-population) specify ``'0'`` or ``'false'``: .. code-block:: console - $ ./manage.py loaddata --populate=false fixtures.json - $ ./manage.py loaddata --populate=0 fixtures.json + $ python manage.py loaddata --populate=false fixtures.json + $ python manage.py loaddata --populate=0 fixtures.json .. note:: - If ``populate`` is not specified, then current auto-population mode is used. *Current* means + If ``populate`` is not specified, the current auto-population mode is used. *Current* means the one set by :ref:`settings `. Moreover, this ``loaddata`` command version can override the nasty habit of changing locale to -`en-us`. By default, it will retain proper locale. To get back to old behaviour, set +`en-us`. By default, it will retain the proper locale. To get the old behaviour back, set :ref:`settings-modeltranslation_loaddata_retain_locale` to ``False``. diff --git a/docs/modeltranslation/conf.py b/docs/modeltranslation/conf.py index a9feb535..d4c63429 100644 --- a/docs/modeltranslation/conf.py +++ b/docs/modeltranslation/conf.py @@ -73,7 +73,7 @@ # General information about the project. project = u'django-modeltranslation' -copyright = u'2009-2014, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' +copyright = u'2009-2015, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -279,7 +279,7 @@ epub_title = u'django-modeltranslation' epub_author = u'Dirk Eschler' epub_publisher = u'Dirk Eschler' -epub_copyright = u'2009-2014, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' +epub_copyright = u'2009-2015, Peter Eschler, Dirk Eschler, Jacek Tomaszewski' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/docs/modeltranslation/contribute.rst b/docs/modeltranslation/contribute.rst index 5c463aa9..40344c26 100644 --- a/docs/modeltranslation/contribute.rst +++ b/docs/modeltranslation/contribute.rst @@ -43,14 +43,14 @@ to be supported in early development stages of a new Django version, we aim to achieve support once it has seen its first release candidate. The supported Python versions can be derived from the supported Django versions. -Example where we support Python 2.5, 2.6 and 2.7: +Example (from the past) where we support Python 2.5, 2.6 and 2.7: * Django 1.3 (old stable) supports Python 2.5, 2.6, 2.7 * Django 1.4 (current stable) supports Python 2.5, 2.6, 2.7 * Django 1.5 (dev) supports Python 2.6, 2.7 -Python 3 is currently not supported, but should be added no later than it becomes -officially supported by Django. +Python 3 is supported since 0.7 release. Although 0.6 release supported Django 1.5 +(which started Python 3 compliance), it was not Python 3 ready yet. Unittests @@ -73,7 +73,15 @@ Continuous Integration The project uses `Travis CI`_ for continuous integration tests. Hooks provided by Github are active, so that each push and pull request is automatically run -against our `Travis CI config`_. +against our `Travis CI config`_, checking code against different databases, +Python and Django versions. This includes automatic tracking of test coverage +through `Coveralls`_. + +.. image:: http://img.shields.io/travis/deschler/django-modeltranslation/master.png?style=flat + :target: https://travis-ci.org/deschler/django-modeltranslation + +.. image:: http://img.shields.io/coveralls/deschler/django-modeltranslation.png?style=flat + :target: https://coveralls.io/r/deschler/django-modeltranslation Contributing Documentation @@ -114,6 +122,7 @@ Please do not use the issue tracker for general questions, we run a dedicated .. _Github: https://github.com/deschler/django-modeltranslation .. _Travis CI: https://travis-ci.org/deschler/django-modeltranslation .. _Travis CI config: https://github.com/deschler/django-modeltranslation/blob/master/.travis.yml +.. _Coveralls: https://coveralls.io/r/deschler/django-modeltranslation .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx-doc.org/ .. _issue tracker: https://github.com/deschler/django-modeltranslation/issues diff --git a/docs/modeltranslation/forms.rst b/docs/modeltranslation/forms.rst index 64f9224a..45787492 100644 --- a/docs/modeltranslation/forms.rst +++ b/docs/modeltranslation/forms.rst @@ -3,6 +3,10 @@ ModelForms ========== +``ModelForms`` for multilanguage models are defined and handled as typical ``ModelForms``. +Please note, however, that they shouldn't be defined next to models +(see :ref:`a note `). + Editing multilanguage models with all translation fields in the admin backend is quite sensible. However, presenting all model fields to the user on the frontend may be not the right way. Here comes the ``TranslationModelForm`` which strip out all translation fields:: @@ -34,18 +38,18 @@ In most cases formfields for translation fields behave as expected. However, the problem with ``models.CharField`` - probably the most commonly translated field type. The problem is that default formfield for ``CharField`` stores empty values as empty strings -(``''``), even if field is nullable +(``''``), even if the field is nullable (see django `ticket #9590 `_). -Thus formfields for translation fields are patched by `MT`. Following rules apply: +Thus formfields for translation fields are patched by modeltranslation. The following rules apply: .. _formfield_rules: -- If original field is not nullable, empty value would be saved as ``''``; -- If original field is nullable, empty value would be saved as ``None``. +- If the original field is not nullable, an empty value is saved as ``''``; +- If the original field is nullable, an empty value is saved as ``None``. To deal with complex cases, these rules can be overridden per model or even per field -(using ``TranslationOptions``):: +using ``TranslationOptions``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) @@ -65,9 +69,9 @@ This configuration is especially useful for fields with unique constraints:: slug = models.SlugField(max_length=30, unique=True) Because the ``slug`` field is not nullable, its translation fields would store empty values as -``''`` and that would result in error when 2 or more ``Categories`` are saved with +``''`` and that would result in an error when two or more ``Categories`` are saved with ``slug_en`` empty - unique constraints wouldn't be satisfied. Instead, ``None`` should be stored, -as several ``None`` values in database don't violate uniqueness:: +as several ``None`` values in the database don't violate uniqueness:: class CategoryTranslationOptions(TranslationOptions): fields = ('name', 'slug') @@ -79,15 +83,15 @@ as several ``None`` values in database don't violate uniqueness:: None-checkbox widget ******************** -Maybe there is a situation when somebody want to store in a field both empty strings and ``None`` -values. For such a scenario there is third configuration value: ``'both'``:: +Maybe there is a situation where you want to store both - empty strings and ``None`` +values - in a field. For such a scenario there is a third configuration value: ``'both'``:: class NewsTranslationOptions(TranslationOptions): fields = ('title', 'text',) empty_values = {'title': None, 'text': 'both'} -It results in special widget with a None-checkbox to null a field. It's not recommended in frontend -as users may be confused what this `None` is. Probably only useful place for this widget is admin -backend; see :ref:`admin-formfield`. +It results in a special widget with a None-checkbox to null a field. It's not recommended in +frontend as users may be confused what this `None` is. The only useful place for this widget might +be the admin backend; see :ref:`admin-formfield`. -To sum up, only valid ``empty_values`` values are: ``None``, ``''`` and ``'both'``. +To sum it up, the valid values for ``empty_values`` are: ``None``, ``''`` and ``'both'``. diff --git a/docs/modeltranslation/installation.rst b/docs/modeltranslation/installation.rst index 64d91702..269a0b3d 100644 --- a/docs/modeltranslation/installation.rst +++ b/docs/modeltranslation/installation.rst @@ -6,21 +6,36 @@ Installation Requirements ------------ -+------------------+------------+-----------+ -| Modeltranslation | Python | Django | -+==================+============+===========+ -| >=0.7 | 3.2 - 3.3 | 1.5 - 1.6 | -| +------------+-----------+ -| | 2.6 - 2.7 | 1.4 - 1.6 | -+------------------+------------+-----------+ -| ==0.5, ==0.6 | 2.6 - 2.7 | 1.5 | -| +------------+-----------+ -| | 2.5 - 2.7 | 1.3 - 1.4 | -+------------------+------------+-----------+ -| ==0.4 | 2.5 - 2.7 | 1.3 - 1.4 | -+------------------+------------+-----------+ -| <=0.3 | 2.4 - 2.7 | 1.0 - 1.4 | -+------------------+------------+-----------+ +Which Modeltranslation version is required for given Django-Python combination to work? + +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +Python Django +------- ----------------------------------------------------------- +version 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +2.4 |u3| |u3| |u3| |u3| +2.5 |u3| |u3| |u3| |36| |36| +2.6 |u3| |u3| |u3| |36| |3| |5| |7| +2.7 |u3| |u3| |u3| |36| |3| |5| |7| |8| |9| |11| |12| |13| +3.2 |7| |7| |8| |9| +3.3 |7| |7| |8| |9| +3.4 |8| |9| |11| |12| |13| +3.5 |9| |11| |12| |13| +3.6 |13| +======= ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== + +(``-X`` denotes "up to version X", whereas ``X+`` means "from version X upwards") + +.. |u3| replace:: -0.3 +.. |3| replace:: 0.3+ +.. |36| replace:: 0.3-0.6 +.. |5| replace:: 0.5+ +.. |7| replace:: 0.7+ +.. |8| replace:: 0.8+ +.. |9| replace:: 0.9+ +.. |11| replace:: 0.11+ +.. |12| replace:: 0.12+ +.. |13| replace:: 0.13+ Using Pip @@ -53,20 +68,24 @@ Setup To setup the application please follow these steps. Each step is described in detail in the following sections: -1. Add the ``modeltranslation`` app to the ``INSTALLED_APPS`` variable of your +1. Add ``modeltranslation`` to the ``INSTALLED_APPS`` variable of your project's ``settings.py``. -#. Set ``USE_I18N = True`` in ``settings.py``. +2. Set ``USE_I18N = True`` in ``settings.py``. -#. Configure your ``LANGUAGES`` in ``settings.py``. +3. Configure your ``LANGUAGES`` in ``settings.py``. -#. Create a ``translation.py`` in your app directory and register +4. Create a ``translation.py`` in your app directory and register ``TranslationOptions`` for every model you want to translate. -#. Sync the database using ``./manage.py syncdb`` (note that this only applies - if the models registered in the ``translation.py`` did not have been - synced to the database before. If they did - read :ref:`further down ` what to do - in that case. +5. Sync the database using ``python manage.py syncdb``. + + .. note:: This only applies if the models registered in ``translation.py`` haven't been + synced to the database before. If they have, please read :ref:`db-fields`. + + .. note:: If you are using Django 1.7 and its internal migration system, run + ``python manage.py makemigrations``, followed by + ``python manage.py migrate`` instead. See :ref:`migrations` for details. Configuration @@ -88,9 +107,21 @@ Make sure that the ``modeltranslation`` app is listed in your INSTALLED_APPS = ( ... 'modeltranslation', + 'django.contrib.admin', # optional .... ) +.. important:: + If you want to use the admin integration, ``modeltranslation`` must be put + before ``django.contrib.admin`` (only applies when using Django 1.7 or + above). + +.. important:: + If you want to use the ``django-debug-toolbar`` together with modeltranslation, use `explicit setup + `_. + Otherwise tweak the order of ``INSTALLED_APPS``: try to put ``debug_toolbar`` as first entry in + ``INSTALLED_APPS`` (in Django < 1.7) or after ``modeltranslation`` (in Django >= 1.7). However, + only `explicit setup` is guaranteed to succeed. .. _settings-languages: @@ -100,10 +131,10 @@ Make sure that the ``modeltranslation`` app is listed in your The ``LANGUAGES`` variable must contain all languages used for translation. The first language is treated as the *default language*. -The modeltranslation application uses the list of languages to add localized -fields to the models registered for translation. To use the languages ``de`` -and ``en`` in your project, set the ``LANGUAGES`` variable like this (where -``de`` is the default language):: +Modeltranslation uses the list of languages to add localized fields to the +models registered for translation. To use the languages ``de`` and ``en`` in +your project, set the ``LANGUAGES`` variable like this (where ``de`` is the +default language):: gettext = lambda s: s LANGUAGES = ( @@ -116,11 +147,20 @@ and ``en`` in your project, set the ``LANGUAGES`` variable like this (where rather required for Django to be able to (statically) translate the verbose names of the languages using the standard ``i18n`` solution. +.. note:: + If, for some reason, you don't want to translate objects to exactly the same languages as + the site would be displayed into, you can set ``MODELTRANSLATION_LANGUAGES`` (see below). + For any language in ``LANGUAGES`` not present in ``MODELTRANSLATION_LANGUAGES``, the *default + language* will be used when accessing translated content. For any language in + ``MODELTRANSLATION_LANGUAGES`` not present in ``LANGUAGES``, probably nobody will see translated + content, since the site wouldn't be accessible in that language. + .. warning:: Modeltranslation does not enforce the ``LANGUAGES`` setting to be defined - in your project. When it isn't present, it defaults to Django's + in your project. When it isn't present (and neither is ``MODELTRANSLATION_LANGUAGES``), it + defaults to Django's `global LANGUAGES setting `_ - instead, and that are quite a number of languages! + instead, and that are quite a few languages! Advanced Settings @@ -147,6 +187,30 @@ Example:: MODELTRANSLATION_DEFAULT_LANGUAGE = 'en' +``MODELTRANSLATION_LANGUAGES`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 0.8 + +Default: same as ``LANGUAGES`` + +Allow to set languages the content will be translated into. If not set, by default all +languages listed in ``LANGUAGES`` will be used. + +Example:: + + LANGUAGES = ( + ('en', 'English'), + ('de', 'German'), + ('pl', 'Polish'), + ) + MODELTRANSLATION_LANGUAGES = ('en', 'de') + +.. note:: + This setting may become useful if your users shall produce content for a restricted + set of languages, while your application is translated into a greater number of locales. + + .. _settings-modeltranslation_fallback_languages: ``MODELTRANSLATION_FALLBACK_LANGUAGES`` @@ -331,4 +395,4 @@ Default: ``True`` Control if the ``loaddata`` command should leave the settings-defined locale alone. Setting it to ``False`` will result in previous behaviour of ``loaddata``: inserting fixtures to database -under `en-us` locale. +under ``en-us`` locale. diff --git a/docs/modeltranslation/registration.rst b/docs/modeltranslation/registration.rst index 3dc5f802..4322f8b9 100644 --- a/docs/modeltranslation/registration.rst +++ b/docs/modeltranslation/registration.rst @@ -12,7 +12,7 @@ steps: 1. Create a ``translation.py`` in your app directory. 2. Create a translation option class for every model to translate. -3. Register the model and the translation option class at the +3. Register the model and the translation option class at ``modeltranslation.translator.translator``. The modeltranslation application reads the ``translation.py`` file in your @@ -33,9 +33,9 @@ Instead of a news, this could be any Django model class:: title = models.CharField(max_length=255) text = models.TextField() -In order to tell the modeltranslation app to translate the ``title`` and -``text`` field, create a ``translation.py`` file in your news app directory and -add the following:: +In order to tell modeltranslation to translate the ``title`` and ``text`` fields, +create a ``translation.py`` file in your news app directory and add the +following:: from modeltranslation.translator import translator, TranslationOptions from news.models import News @@ -50,6 +50,20 @@ only imported. The ``NewsTranslationOptions`` derives from ``TranslationOptions`` and provides the ``fields`` attribute. Finally the model and its translation options are registered at the ``translator`` object. +.. versionadded:: 0.10 + +If you prefer, ``register`` is also available as a decorator, much like the +one Django introduced for its admin in version 1.7. Usage is similar to the +standard ``register``, just provide arguments as you normally would, except +the options class which will be the decorated one:: + + from modeltranslation.translator import register, TranslationOptions + from news.models import News + + @register(News) + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + At this point you are mostly done and the model classes registered for translation will have been added some auto-magical fields. The next section explains how things are working under the hood. @@ -86,7 +100,7 @@ say it in code:: Of course multiple inheritance and inheritance chains (A > B > C) also work as expected. -.. note:: When upgrading from a previous modeltranslation version, please +.. note:: When upgrading from a previous modeltranslation version (<0.5), please review your ``TranslationOptions`` classes and see if introducing `fields inheritance` broke the project (if you had always subclassed ``TranslationOptions`` only, there is no risk). @@ -128,6 +142,29 @@ As these fields are added to the registered model class as fully valid Django model fields, they will appear in the db schema for the model although it has not been specified on the model explicitly. +.. _register-precautions: + +Precautions regarding registration approach +******************************************* + +Be aware that registration approach (as opposed to base-class approach) to +models translation has a few caveats, though (despite many pros). + +First important thing to note is the fact that translatable models are being patched - that means +their fields list is not final until the modeltranslation code executes. In normal circumstances +it shouldn't affect anything - as long as ``models.py`` contain only models' related code. + +For example: consider a project where a ``ModelForm`` is declared in ``models.py`` just after +its model. When the file is executed, the form gets prepared - but it will be frozen with +old fields list (without translation fields). That's because the ``ModelForm`` will be created +before modeltranslation would add new fields to the model (``ModelForm`` gather fields info at class +creation time, not instantiation time). Proper solution is to define the form in ``forms.py``, +which wouldn't be imported alongside with ``models.py`` (and rather imported from views file or +urlconf). + +Generally, for seamless integration with modeltranslation (and as sensible design anyway), +the ``models.py`` should contain only bare models and model related logic. + .. _db-fields: Committing fields to database @@ -146,23 +183,92 @@ fields) and apply it. If not, you can use a little helper: :ref:`commands-sync_translation_fields` which can execute schema-ALTERing SQL to add new fields. Use either of these two solutions, not both. -If you are adding translation fields to third-party app that is using South, -things get more complicated. In order to be able to update the app in future, +If you are adding translation fields to a third-party app that is using South, +things get more complicated. In order to be able to update the app in the future, and to feel comfortable, you should use the ``sync_translation_fields`` command. Although it's possible to introduce new fields in a migration, it's nasty and involves copying migration files, using ``SOUTH_MIGRATION_MODULES`` setting, and passing ``--delete-ghost-migrations`` flag, so we don't recommend it. Invoking ``sync_translation_fields`` is plain easier. -Note that all added fields are -declared ``blank=True`` and ``null=True`` no matter if the original field is -required or not. In other words - all translations are optional. To populate -the default translation fields added by the modeltranslation application -with values from existing database fields, you -can use the ``update_translation_fields`` command below. See +Note that all added fields are by default declared ``blank=True`` and +``null=True`` no matter if the original field is required or not. In other +words - all translations are optional, unless an explicit option is +provided - see :ref:`required_langs`. + +To populate the default translation fields added by modeltranslation with +values from existing database fields, you can use the +``update_translation_fields`` command. See :ref:`commands-update_translation_fields` for more info on this. +.. _migrations: + +Migrations (Django 1.7) +^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 0.8 + +Modeltranslation supports the migration system introduced by Django 1.7. +Besides the normal workflow as described in Django's `Migration docs`_, you +should do a migration whenever one of the following changes have been made to +your project: + +- Added or removed a language through ``settings.LANGUAGES`` or + ``settings.MODELTRANSLATION LANGUAGES``. +- Registered or unregistered a field through ``TranslationOptions.fields``. + +It doesn't matter if you are starting a fresh project or change an existing +one, it's always: + +1. ``python manage.py makemigrations`` to create a new migration with + the added or removed fields. +2. ``python manage.py migrate`` to apply the changes. + +.. As opposed to the statement made in :ref:`db-fields`, using the +.. :ref:`sync_translation_fields ` +.. management command together with the new migration system is not recommended. + +.. note:: + Support for migrations is implemented through + ``fields.TranslationField.deconstruct(self)`` and respects changes to the + ``null`` option. + + +.. _required_langs: + +Required fields +--------------- + +.. versionadded:: 0.8 + +By default, all translation fields are optional (not required). This can be +changed using a special attribute on ``TranslationOptions``:: + + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + required_languages = ('en', 'de') + +It's quite self-explanatory: for German and English, all translation fields are required. For other +languages - optional. + +A more fine-grained control is available:: + + class NewsTranslationOptions(TranslationOptions): + fields = ('title', 'text',) + required_languages = {'de': ('title', 'text'), 'default': ('title',)} + +For German, all fields (both ``title`` and ``text``) are required; for all other languages - only +``title`` is required. The ``'default'`` is optional. + +.. note:: + Requirement is enforced by ``blank=False``. Please remember that it will trigger validation only + in modelforms and admin (as always in Django). Manual model validation can be performed via + the ``full_clean()`` model method. + + The required fields are still ``null=True``, though. + + ``TranslationOptions`` attributes reference ------------------------------------------- @@ -205,6 +311,13 @@ Classes inheriting from ``TranslationOptions`` can have following attributes def empty_values = '' empty_values = {'title': '', 'slug': None, 'desc': 'both'} +.. attribute:: TranslationOptions.required_languages + + Control which translation fields are required. See :ref:`required_langs`. :: + + required_languages = ('en', 'de') + required_languages = {'de': ('title','text'), 'default': ('title',)} + .. _supported_field_matrix: @@ -254,3 +367,6 @@ Model Field 0.4 0.5 0.7 .. |u| replace:: ? \* Implicitly supported (as subclass of a supported field) + + +.. _Migration docs: https://docs.djangoproject.com/en/dev/topics/migrations/#workflow diff --git a/docs/modeltranslation/usage.rst b/docs/modeltranslation/usage.rst index 037c01a7..9bbe7847 100644 --- a/docs/modeltranslation/usage.rst +++ b/docs/modeltranslation/usage.rst @@ -3,7 +3,7 @@ Accessing Translated and Translation Fields =========================================== -The modeltranslation app changes the behaviour of the translated fields. To +Modeltranslation changes the behaviour of the translated fields. To explain this consider the news example from the :ref:`registration` chapter again. The original ``News`` model looked like this:: @@ -11,7 +11,7 @@ again. The original ``News`` model looked like this:: title = models.CharField(max_length=255) text = models.TextField() -Now that it is registered with the modeltranslation app the model looks +Now that it is registered with modeltranslation the model looks like this - note the additional fields automatically added by the app:: class News(models.Model): @@ -123,8 +123,9 @@ These manager methods perform rewriting: - ``order_by()`` - ``update()`` - ``only()``, ``defer()`` -- ``values()``, ``values_list()`` +- ``values()``, ``values_list()``, with :ref:`fallback ` mechanism - ``dates()`` +- ``select_related()`` - ``create()``, with optional auto-population_ feature In order not to introduce differences between ``X.objects.create(...)`` and ``X(...)``, model @@ -159,21 +160,22 @@ Moreover, some fields can be explicitly assigned different values:: It will result in ``title_de == 'enigma'`` and other ``title_?? == '-- no translation yet --'``. -There is another way of altering the current population status, an ``auto_populate`` context manager:: +There is another way of altering the current population status, an ``auto_populate`` context +manager:: from modeltranslation.utils import auto_populate with auto_populate(True): x = News.objects.create(title='bar') -Auto-population tooks place also in model constructor, what is extremely useful when loading +Auto-population takes place also in model constructor, what is extremely useful when loading non-translated fixtures. Just remember to use the context manager:: with auto_populate(): # True can be ommited - call_command('loaddata', 'fixture.json') # Some fixture loading + call_command('loaddata', 'fixture.json') # Some fixture loading - z = News(title='bar') - print z.title_en, z.title_de # prints 'bar bar' + z = News(title='bar') + print z.title_en, z.title_de # prints 'bar bar' There is a more convenient way than calling ``populate`` manager method or entering ``auto_populate`` manager context all the time: @@ -185,7 +187,7 @@ It controls the default population behaviour. Auto-population modes ^^^^^^^^^^^^^^^^^^^^^ -There are 4 different population modes: +There are four different population modes: ``False`` [set by default] @@ -211,19 +213,26 @@ There are 4 different population modes: Falling back ------------ -Modeltranslation provides mechanism to control behaviour of data access in case of empty -translation values. This mechanism affects field access. +Modeltranslation provides a mechanism to control behaviour of data access in case of empty +translation values. This mechanism affects field access, as well as ``values()`` +and ``values_list()`` manager methods. -Consider ``News`` example: a creator of some news hasn't specified it's german title and content, -but only english ones. Then if a german visitor is viewing site, we would rather show him english -title/content of the news than display empty strings. This is called *fallback*. :: +Consider the ``News`` example: a creator of some news hasn't specified its German title and +content, but only English ones. Then if a German visitor is viewing the site, we would rather show +him English title/content of the news than display empty strings. This is called *fallback*. :: - News.title_en = 'English title' - News.title_de = '' - print News.title - # If current active language is german, it should display title_de field value (''). + news.title_en = 'English title' + news.title_de = '' + print news.title + # If current active language is German, it should display the title_de field value (''). # But if fallback is enabled, it would display 'English title' instead. + # Similarly for manager + news.save() + print News.objects.filter(pk=news.pk).values_list('title', flat=True)[0] + # As above: if current active language is German and fallback to English is enabled, + # it would display 'English title'. + There are several ways of controlling fallback, described below. .. _fallback_lang: @@ -233,17 +242,17 @@ Fallback languages .. versionadded:: 0.5 -:ref:`settings-modeltranslation_fallback_languages` setting allows to set order of *fallback -languages*. By default it is only ``DEFAULT_LANGUAGE``. +:ref:`settings-modeltranslation_fallback_languages` setting allows to set the order of *fallback +languages*. By default that's the ``DEFAULT_LANGUAGE``. For example, setting :: MODELTRANSLATION_FALLBACK_LANGUAGES = ('en', 'de', 'fr') -means: if current active language field value is unset, try english value. If it is also unset, -try german, and so on - until some language yield non-empty value of the field. +means: if current active language field value is unset, try English value. If it is also unset, +try German, and so on - until some language yields a non-empty value of the field. -There is also option to define fallback by language, using dict syntax:: +There is also an option to define a fallback by language, using dict syntax:: MODELTRANSLATION_FALLBACK_LANGUAGES = { 'default': ('en', 'de', 'fr'), @@ -254,11 +263,11 @@ There is also option to define fallback by language, using dict syntax:: The ``default`` key is required and its value denote languages which are always tried at the end. With such a setting: -- for `uk` (Ukrainian) order of fallback languages is: ``('ru', 'en', 'de', 'fr')`` -- for `fr` order of fallback languages is: ``('de', 'en')`` - `fr` obviously is not fallback, since - it's active language; and `de` would be tried before `en` -- for `en` and `de` fallback order is ``('de', 'fr')`` and ``('en', 'fr')``, respectively -- for any other language order of fallback languages is just ``('en', 'de', 'fr')`` +- for `uk` the order of fallback languages is: ``('ru', 'en', 'de', 'fr')`` +- for `fr` the order of fallback languages is: ``('de', 'en')`` - Note, that `fr` obviously is not + a fallback, since its active language and `de` would be tried before `en` +- for `en` and `de` the fallback order is ``('de', 'fr')`` and ``('en', 'fr')``, respectively +- for any other language the order of fallback languages is just ``('en', 'de', 'fr')`` What is more, fallback languages order can be overridden per model, using ``TranslationOptions``:: @@ -285,7 +294,7 @@ Fallback values .. versionadded:: 0.4 But what if current language and all fallback languages yield no field value? Then modeltranslation -will use field's *fallback value*, if one was defined. +will use the field's *fallback value*, if one was defined. Fallback values are defined in ``TranslationOptions``, for example:: @@ -307,7 +316,7 @@ Fallback values can be also customized per model field:: } If current language and all fallback languages yield no field value, and no fallback values are -defined, then modeltranslation will use field's default value. +defined, then modeltranslation will use the field's default value. .. _fallback_undef: @@ -318,13 +327,13 @@ Fallback undefined Another question is what do we consider "no value", on what value should we fall back to other translations? For text fields the empty string can usually be considered as the undefined value, -but other fields may have different concepts of empty or missing value. +but other fields may have different concepts of empty or missing values. Modeltranslation defaults to using the field's default value as the undefined value (the empty string for non-nullable ``CharFields``). This requires calling ``get_default`` for every field access, which in some cases may be expensive. -If you'd like to fallback on a different value or your default is expensive to calculate, provide +If you'd like to fall back on a different value or your default is expensive to calculate, provide a custom undefined value (for a field or model):: class NewsTranslationOptions(TranslationOptions): @@ -338,6 +347,7 @@ The State of the Original Field ------------------------------- .. versionchanged:: 0.5 +.. versionchanged:: 0.12 As defined by the :ref:`rules`, accessing the original field is guaranteed to work on the associated translation field of the current language. This applies @@ -350,6 +360,9 @@ Attempts to keep the value in sync with either the default or current language's field value has raised a boatload of unpredictable side effects in older versions of modeltranslation. +Since version 0.12 the original field is expected to have even more undetermined value. +It's because Django 1.10 changed the way deferred fields work. + .. warning:: Do not rely on the underlying value of the *original field* in any way! diff --git a/modeltranslation/__init__.py b/modeltranslation/__init__.py index a043fcbb..188cd00d 100644 --- a/modeltranslation/__init__.py +++ b/modeltranslation/__init__.py @@ -3,7 +3,8 @@ Version code adopted from Django development version. https://github.com/django/django """ -VERSION = (0, 7, 3, 'final', 0) +VERSION = (0, 12, 2, 'final', 0) +default_app_config = 'modeltranslation.apps.ModeltranslationConfig' def get_version(version=None): @@ -31,7 +32,7 @@ def get_version(version=None): sub = '.dev%s' % git_changeset elif version[3] != 'final': - mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'c'} + mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'} sub = mapping[version[3]] + str(version[4]) return str(main + sub) diff --git a/modeltranslation/admin.py b/modeltranslation/admin.py old mode 100644 new mode 100755 index 88240e35..d2ed19a0 --- a/modeltranslation/admin.py +++ b/modeltranslation/admin.py @@ -1,21 +1,29 @@ # -*- coding: utf-8 -*- from copy import deepcopy +import django from django.contrib import admin from django.contrib.admin.options import BaseModelAdmin, flatten_fieldsets, InlineModelAdmin -from django.contrib.contenttypes import generic from django import forms -# Ensure that models are registered for translation before TranslationAdmin -# runs. The import is supposed to resolve a race condition between model import -# and translation registration in production (see issue #19). -import modeltranslation.models # NOQA from modeltranslation import settings as mt_settings from modeltranslation.translator import translator from modeltranslation.utils import ( - get_translation_fields, build_css_class, build_localized_fieldname, get_language, unique) + get_translation_fields, build_css_class, build_localized_fieldname, get_language, + get_language_bidi, unique) from modeltranslation.widgets import ClearableWidgetWrapper +# Ensure that models are registered for translation before TranslationAdmin +# runs. The import is supposed to resolve a race condition between model import +# and translation registration in production (see issue #19). +if django.VERSION < (1, 7): + from django.contrib.contenttypes.generic import GenericTabularInline + from django.contrib.contenttypes.generic import GenericStackedInline + import modeltranslation.models # NOQA +else: + from django.contrib.contenttypes.admin import GenericTabularInline + from django.contrib.contenttypes.admin import GenericStackedInline + class TranslationBaseModelAdmin(BaseModelAdmin): _orig_was_required = {} @@ -67,17 +75,22 @@ def patch_translation_field(self, db_field, field, **kwargs): b for b in form_class.__bases__ if b != NullCharField) field.__class__ = type( 'Nullable%s' % form_class.__name__, (NullableField, form_class), {}) - if ((db_field.empty_value == 'both' or orig_field.name in self.both_empty_values_fields) - and isinstance(field.widget, (forms.TextInput, forms.Textarea))): + if ( + ( + db_field.empty_value == 'both' or + orig_field.name in self.both_empty_values_fields + ) and isinstance(field.widget, (forms.TextInput, forms.Textarea)) + ): field.widget = ClearableWidgetWrapper(field.widget) css_classes = field.widget.attrs.get('class', '').split(' ') css_classes.append('mt') # Add localized fieldname css class css_classes.append(build_css_class(db_field.name, 'mt-field')) - + # Add mt-bidi css class if language is bidirectional + if(get_language_bidi(db_field.language)): + css_classes.append('mt-bidi') if db_field.language == mt_settings.DEFAULT_LANGUAGE: - # Add another css class to identify a default modeltranslation - # widget. + # Add another css class to identify a default modeltranslation widget css_classes.append('mt-default') if (orig_formfield.required or self._orig_was_required.get( '%s.%s' % (orig_field.model._meta, orig_field.name))): @@ -167,9 +180,9 @@ def append_lang(source): prepopulated_fields[dest] = localize(sources, lang) self.prepopulated_fields = prepopulated_fields - def _do_get_form_or_formset(self, request, obj, **kwargs): + def _get_form_or_formset(self, request, obj, **kwargs): """ - Code shared among get_form and get_formset. + Generic code shared by get_form and get_formset. """ if self.exclude is None: exclude = [] @@ -188,17 +201,17 @@ def _do_get_form_or_formset(self, request, obj, **kwargs): return kwargs - def _do_get_fieldsets_pre_form_or_formset(self): + def _get_fieldsets_pre_form_or_formset(self): """ - Common get_fieldsets code shared among TranslationAdmin and - TranslationInlineModelAdmin. + Generic get_fieldsets code, shared by + TranslationAdmin and TranslationInlineModelAdmin. """ return self._declared_fieldsets() - def _do_get_fieldsets_post_form_or_formset(self, request, form, obj=None): + def _get_fieldsets_post_form_or_formset(self, request, form, obj=None): """ - Common get_fieldsets code shared among TranslationAdmin and - TranslationInlineModelAdmin. + Generic get_fieldsets code, shared by + TranslationAdmin and TranslationInlineModelAdmin. """ base_fields = self.replace_orig_field(form.base_fields.keys()) fields = base_fields + list(self.get_readonly_fields(request, obj)) @@ -206,8 +219,9 @@ def _do_get_fieldsets_post_form_or_formset(self, request, form, obj=None): def get_translation_field_excludes(self, exclude_languages=None): """ - Returns a tuple of translation field names to exclude base on + Returns a tuple of translation field names to exclude based on `exclude_languages` arg. + TODO: Currently unused? """ if exclude_languages is None: exclude_languages = [] @@ -217,14 +231,13 @@ def get_translation_field_excludes(self, exclude_languages=None): exclude = [] for orig_fieldname, translation_fields in self.trans_opts.fields.items(): for tfield in translation_fields: - language = tfield.name.split('_')[-1] - if language in excl_languages and tfield not in exclude: + if tfield.language in excl_languages and tfield not in exclude: exclude.append(tfield) return tuple(exclude) def get_readonly_fields(self, request, obj=None): """ - Hook for specifying custom readonly fields. + Hook to specify custom readonly fields. """ return self.replace_orig_field(self.readonly_fields) @@ -263,15 +276,15 @@ def _group_fieldsets(self, fieldsets): untranslated_fields = [ f.name for f in self.opts.fields if ( # Exclude the primary key field - f is not self.opts.auto_field + f is not self.opts.auto_field and # Exclude non-editable fields - and f.editable + f.editable and # Exclude the translation fields - and not hasattr(f, 'translated_field') + not hasattr(f, 'translated_field') and # Honour field arguments. We rely on the fact that the # passed fieldsets argument is already fully filtered # and takes options like exclude into account. - and f.name in flattened_fieldsets + f.name in flattened_fieldsets ) ] # TODO: Allow setting a label @@ -284,7 +297,7 @@ def _group_fieldsets(self, fieldsets): # Extract the original field's verbose_name for use as this # fieldset's label - using ugettext_lazy in your model # declaration can make that translatable. - label = self.model._meta.get_field(orig_field).verbose_name + label = self.model._meta.get_field(orig_field).verbose_name.capitalize() temp_fieldsets[orig_field] = (label, { 'fields': trans_fieldnames, 'classes': ('mt-fieldset',) @@ -299,20 +312,20 @@ def _group_fieldsets(self, fieldsets): return fieldsets def get_form(self, request, obj=None, **kwargs): - kwargs = self._do_get_form_or_formset(request, obj, **kwargs) + kwargs = self._get_form_or_formset(request, obj, **kwargs) return super(TranslationAdmin, self).get_form(request, obj, **kwargs) def get_fieldsets(self, request, obj=None): if self.declared_fieldsets: - return self._do_get_fieldsets_pre_form_or_formset() + return self._get_fieldsets_pre_form_or_formset() return self._group_fieldsets( - self._do_get_fieldsets_post_form_or_formset( + self._get_fieldsets_post_form_or_formset( request, self.get_form(request, obj, fields=None), obj)) class TranslationInlineModelAdmin(TranslationBaseModelAdmin, InlineModelAdmin): def get_formset(self, request, obj=None, **kwargs): - kwargs = self._do_get_form_or_formset(request, obj, **kwargs) + kwargs = self._get_form_or_formset(request, obj, **kwargs) return super(TranslationInlineModelAdmin, self).get_formset(request, obj, **kwargs) def get_fieldsets(self, request, obj=None): @@ -320,9 +333,9 @@ def get_fieldsets(self, request, obj=None): # fieldset line with just the original model verbose_name of the model # is displayed above the new fieldsets. if self.declared_fieldsets: - return self._do_get_fieldsets_pre_form_or_formset() + return self._get_fieldsets_pre_form_or_formset() form = self.get_formset(request, obj, fields=None).form - return self._do_get_fieldsets_post_form_or_formset(request, form, obj) + return self._get_fieldsets_post_form_or_formset(request, form, obj) class TranslationTabularInline(TranslationInlineModelAdmin, admin.TabularInline): @@ -333,34 +346,67 @@ class TranslationStackedInline(TranslationInlineModelAdmin, admin.StackedInline) pass -class TranslationGenericTabularInline(TranslationInlineModelAdmin, generic.GenericTabularInline): +class TranslationGenericTabularInline(TranslationInlineModelAdmin, GenericTabularInline): pass -class TranslationGenericStackedInline(TranslationInlineModelAdmin, generic.GenericStackedInline): +class TranslationGenericStackedInline(TranslationInlineModelAdmin, GenericStackedInline): pass +class TabbedDjango15JqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary static files for tabbed + translation fields. Reuses Django's internal jquery version. Django 1.5 + included jquery 1.4.2 which is known to work well with jquery-ui 1.8.2. + """ + class Media: + js = ( + 'modeltranslation/js/force_jquery.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', + 'modeltranslation/js/tabbed_translation_fields.js', + ) + css = { + 'all': ('modeltranslation/css/tabbed_translation_fields.css',), + } + + class TabbedDjangoJqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary media files for tabbed + translation fields. Reuses Django's internal jquery version. + """ class Media: js = ( 'modeltranslation/js/force_jquery.js', - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.2/jquery-ui.min.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { 'all': ('modeltranslation/css/tabbed_translation_fields.css',), } -TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin class TabbedExternalJqueryTranslationAdmin(TranslationAdmin): + """ + Convenience class which includes the necessary media files for tabbed + translation fields. Loads recent jquery version from a cdn. + """ class Media: js = ( - 'http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', - 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js', + '//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js', + '//ajax.googleapis.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js', + '//cdn.jsdelivr.net/jquery.mb.browser/0.1/jquery.mb.browser.min.js', 'modeltranslation/js/tabbed_translation_fields.js', ) css = { 'screen': ('modeltranslation/css/tabbed_translation_fields.css',), } + + +if django.VERSION < (1, 6): + TabbedTranslationAdmin = TabbedDjango15JqueryTranslationAdmin +else: + TabbedTranslationAdmin = TabbedDjangoJqueryTranslationAdmin diff --git a/modeltranslation/apps.py b/modeltranslation/apps.py new file mode 100644 index 00000000..7f4d49ba --- /dev/null +++ b/modeltranslation/apps.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from django.apps import AppConfig + + +class ModeltranslationConfig(AppConfig): + name = 'modeltranslation' + verbose_name = 'Modeltranslation' + + def ready(self): + from modeltranslation.models import handle_translation_registrations + handle_translation_registrations() diff --git a/modeltranslation/decorators.py b/modeltranslation/decorators.py new file mode 100644 index 00000000..ed8f0a74 --- /dev/null +++ b/modeltranslation/decorators.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + + +def register(model_or_iterable, **options): + """ + Registers the given model(s) with the given translation options. + + The model(s) should be Model classes, not instances. + + Fields declared for translation on a base class are inherited by + subclasses. If the model or one of its subclasses is already + registered for translation, this will raise an exception. + + @register(Author) + class AuthorTranslation(TranslationOptions): + pass + """ + from modeltranslation.translator import translator, TranslationOptions + + def wrapper(opts_class): + if not issubclass(opts_class, TranslationOptions): + raise ValueError('Wrapped class must subclass TranslationOptions.') + translator.register(model_or_iterable, opts_class, **options) + return opts_class + + return wrapper diff --git a/modeltranslation/fields.py b/modeltranslation/fields.py index 3442fdf8..9ded57b2 100644 --- a/modeltranslation/fields.py +++ b/modeltranslation/fields.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- +from django import VERSION from django import forms from django.core.exceptions import ImproperlyConfigured from django.db.models import fields +from django.utils import six from modeltranslation import settings as mt_settings from modeltranslation.utils import ( @@ -22,6 +24,7 @@ fields.FloatField, fields.DecimalField, fields.IPAddressField, + fields.GenericIPAddressField, fields.DateField, fields.DateTimeField, fields.TimeField, @@ -30,10 +33,8 @@ fields.related.ForeignKey, # Above implies also OneToOneField ) -try: - SUPPORTED_FIELDS += (fields.GenericIPAddressField,) # Django 1.4+ only -except AttributeError: - pass + +NEW_RELATED_API = VERSION >= (1, 9) class NONE: @@ -98,6 +99,8 @@ class TranslationField(object): that needs to be specified when the field is created. """ def __init__(self, translated_field, language, empty_value, *args, **kwargs): + from modeltranslation.translator import translator + # Update the dict of this field with the content of the original one # This might be a bit radical?! Seems to work though... self.__dict__.update(translated_field.__dict__) @@ -109,25 +112,48 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): if empty_value is NONE: self.empty_value = None if translated_field.null else '' - # Translation are always optional (for now - maybe add some parameters - # to the translation options for configuring this) - + # Default behaviour is that all translations are optional if not isinstance(self, fields.BooleanField): # TODO: Do we really want to enforce null *at all*? Shouldn't this # better honour the null setting of the translated field? self.null = True self.blank = True + # Take required_languages translation option into account + trans_opts = translator.get_options_for_model(self.model) + if trans_opts.required_languages: + required_languages = trans_opts.required_languages + if isinstance(trans_opts.required_languages, (tuple, list)): + # All fields + if self.language in required_languages: + # self.null = False + self.blank = False + else: + # Certain fields only + # Try current language - if not present, try 'default' key + try: + req_fields = required_languages[self.language] + except KeyError: + req_fields = required_languages.get('default', ()) + if self.name in req_fields: + # TODO: We might have to handle the whole thing through the + # FieldsAggregationMetaClass, as fields can be inherited. + # self.null = False + self.blank = False + # Adjust the name of this field to reflect the language - self.attname = build_localized_fieldname(self.translated_field.name, self.language) + self.attname = build_localized_fieldname(self.translated_field.name, language) self.name = self.attname + if self.translated_field.db_column: + self.db_column = build_localized_fieldname(self.translated_field.db_column, language) + self.column = self.db_column # Copy the verbose name and append a language suffix # (will show up e.g. in the admin). self.verbose_name = build_localized_verbose_name(translated_field.verbose_name, language) # ForeignKey support - rewrite related_name - if self.rel and self.related and not self.rel.is_hidden(): + if not NEW_RELATED_API and self.rel and self.related and not self.rel.is_hidden(): import copy current = self.related.get_accessor_name() self.rel = copy.copy(self.rel) # Since fields cannot share the same rel object. @@ -143,6 +169,21 @@ def __init__(self, translated_field, language, empty_value, *args, **kwargs): self.rel.field = self # Django 1.6 if hasattr(self.rel.to._meta, '_related_objects_cache'): del self.rel.to._meta._related_objects_cache + elif NEW_RELATED_API and self.remote_field and not self.remote_field.is_hidden(): + import copy + current = self.remote_field.get_accessor_name() + # Since fields cannot share the same rel object: + self.remote_field = copy.copy(self.remote_field) + + if self.remote_field.related_name is None: + # For implicit related_name use different query field name + loc_related_query_name = build_localized_fieldname( + self.related_query_name(), self.language) + self.related_query_name = lambda: loc_related_query_name + self.remote_field.related_name = build_localized_fieldname(current, self.language) + self.remote_field.field = self # Django 1.6 + if hasattr(self.remote_field.model._meta, '_related_objects_cache'): + del self.remote_field.model._meta._related_objects_cache # Django 1.5 changed definition of __hash__ for fields to be fine with hash requirements. # It spoiled our machinery, since TranslationField has the same creation_counter as its @@ -161,14 +202,6 @@ def __ne__(self, other): def __hash__(self): return hash((self.creation_counter, self.language)) - def get_attname_column(self): - attname = self.get_attname() - if self.translated_field.db_column: - column = build_localized_fieldname(self.translated_field.db_column, self.language) - else: - column = attname - return attname, column - def formfield(self, *args, **kwargs): """ Returns proper formfield, according to empty_values setting @@ -227,6 +260,20 @@ def save_form_data(self, instance, data, check=True): else: super(TranslationField, self).save_form_data(instance, data) + def deconstruct(self): + name, path, args, kwargs = self.translated_field.deconstruct() + if self.null is True: + kwargs.update({'null': True}) + if 'db_column' in kwargs: + kwargs['db_column'] = self.db_column + return six.text_type(self.name), path, args, kwargs + + def clone(self): + from django.utils.module_loading import import_string + name, path, args, kwargs = self.deconstruct() + cls = import_string(path) + return cls(*args, **kwargs) + def south_field_triple(self): """ Returns a suitable description of this field for South. @@ -263,6 +310,10 @@ def __set__(self, instance, value): """ Updates the translation field for the current language. """ + # In order for deferred fields to work, we also need to set the base value + instance.__dict__[self.field.name] = value + if isinstance(self.field, fields.related.ForeignKey): + instance.__dict__[self.field.get_attname()] = None if value is None else value.pk if getattr(instance, '_mt_init', False): # When assignment takes place in model instance constructor, don't set value. # This is essential for only/defer to work, but I think it's sensible anyway. @@ -328,6 +379,8 @@ def __set__(self, instance, value): # Localized field name with '_id' loc_attname = instance._meta.get_field(loc_field_name).get_attname() setattr(instance, loc_attname, value) + base_attname = instance._meta.get_field(self.field_name).get_attname() + instance.__dict__[base_attname] = value def __get__(self, instance, owner): if instance is None: diff --git a/modeltranslation/forms.py b/modeltranslation/forms.py index f74c2c99..9e529be2 100644 --- a/modeltranslation/forms.py +++ b/modeltranslation/forms.py @@ -35,6 +35,13 @@ def to_python(self, value): # Django 1.6 def _has_changed(self, initial, data): + return self.has_changed(initial, data) + + def has_changed(self, initial, data): if (initial is None and data is not None) or (initial is not None and data is None): return True - return super(NullableField, self)._has_changed(initial, data) + obj = super(NullableField, self) + if hasattr(obj, 'has_changed'): + return obj.has_changed(initial, data) + else: # Django < 1.9 compat + return obj._has_changed(initial, data) diff --git a/modeltranslation/management/commands/loaddata.py b/modeltranslation/management/commands/loaddata.py index 5912bd0d..b936eefd 100644 --- a/modeltranslation/management/commands/loaddata.py +++ b/modeltranslation/management/commands/loaddata.py @@ -1,5 +1,3 @@ -from optparse import make_option, OptionValueError - from django import VERSION from django.core.management.commands.loaddata import Command as LoadDataCommand @@ -13,23 +11,36 @@ ALLOWED_FOR_PRINT = ', '.join(str(i) for i in (0, ) + ALLOWED[1:]) # For pretty-printing -def check_mode(option, opt_str, value, parser): +def check_mode(option, opt_str, value, parser, namespace=None): if value == '0' or value.lower() == 'false': value = False if value not in ALLOWED: - raise OptionValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT)) - setattr(parser.values, option.dest, value) + raise ValueError("%s option can be only one of: %s" % (opt_str, ALLOWED_FOR_PRINT)) + setattr(namespace or parser.values, option.dest, value) class Command(LoadDataCommand): leave_locale_alone = mt_settings.LOADDATA_RETAIN_LOCALE # Django 1.6 - option_list = LoadDataCommand.option_list + ( - make_option('--populate', action='callback', callback=check_mode, dest='populate', - type='string', - metavar='MODE', help='Using this option will cause fixtures to be loaded under ' - 'auto-population MODE. Allowed values are: %s' % ALLOWED_FOR_PRINT), - ) + help = ('Using this option will cause fixtures to be loaded under auto-population MODE.' + + 'Allowed values are: %s' % ALLOWED_FOR_PRINT) + if VERSION < (1, 8): + from optparse import make_option + option_list = LoadDataCommand.option_list + ( + make_option('--populate', action='callback', callback=check_mode, type='string', + dest='populate', metavar='MODE', help=help), + ) + else: + import argparse + + class CheckAction(argparse.Action): + def __call__(self, parser, namespace, value, option_string=None): + check_mode(self, option_string, value, parser, namespace) + + def add_arguments(self, parser): + super(Command, self).add_arguments(parser) + parser.add_argument('--populate', action=self.CheckAction, type=str, dest='populate', + metavar='MODE', help=self.help) def __init__(self): super(Command, self).__init__() @@ -38,7 +49,7 @@ def __init__(self): self.locale = translation.get_language() def handle(self, *fixture_labels, **options): - if self.can_import_settings and hasattr(self, 'locale'): + if hasattr(self, 'locale'): from django.utils import translation translation.activate(self.locale) diff --git a/modeltranslation/management/commands/sync_translation_fields.py b/modeltranslation/management/commands/sync_translation_fields.py index ea309f65..5518ae08 100644 --- a/modeltranslation/management/commands/sync_translation_fields.py +++ b/modeltranslation/management/commands/sync_translation_fields.py @@ -9,13 +9,14 @@ Credits: Heavily inspired by django-transmeta's sync_transmeta_db command. """ -from optparse import make_option -from django.conf import settings -from django.core.management.base import NoArgsCommand +import django +from django import VERSION +from django.core.management.base import BaseCommand from django.core.management.color import no_style from django.db import connection, transaction from django.utils.six import moves +from modeltranslation.settings import AVAILABLE_LANGUAGES from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname @@ -45,17 +46,23 @@ def print_missing_langs(missing_langs, field_name, model_name): field_name, model_name, ", ".join(missing_langs))) -class Command(NoArgsCommand): +class Command(BaseCommand): help = ('Detect new translatable fields or new available languages and' ' sync database structure. Does not remove columns of removed' ' languages or undeclared fields.') - option_list = NoArgsCommand.option_list + ( - make_option('--noinput', action='store_false', dest='interactive', default=True, - help='Do NOT prompt the user for input of any kind.'), - ) - - def handle_noargs(self, **options): + if VERSION < (1, 8): + from optparse import make_option + option_list = BaseCommand.option_list + ( + make_option('--noinput', action='store_false', dest='interactive', default=True, + help='Do NOT prompt the user for input of any kind.'), + ) + else: + def add_arguments(self, parser): + parser.add_argument('--noinput', action='store_false', dest='interactive', default=True, + help='Do NOT prompt the user for input of any kind.'), + + def handle(self, *args, **options): """ Command execution. """ @@ -67,10 +74,17 @@ def handle_noargs(self, **options): models = translator.get_registered_models(abstract=False) for model in models: db_table = model._meta.db_table - model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name) + if django.VERSION < (1, 8): + model_name = model._meta.module_name + else: + model_name = model._meta.model_name + model_full_name = '%s.%s' % (model._meta.app_label, model_name) opts = translator.get_options_for_model(model) - for field_name in opts.local_fields.keys(): - missing_langs = list(self.get_missing_languages(field_name, db_table)) + for field_name, fields in opts.local_fields.items(): + # Take `db_column` attribute into account + field = list(fields)[0] + column_name = field.db_column if field.db_column else field_name + missing_langs = list(self.get_missing_languages(column_name, db_table)) if missing_langs: found_missing_fields = True print_missing_langs(missing_langs, field_name, model_full_name) @@ -85,7 +99,8 @@ def handle_noargs(self, **options): else: print('SQL not executed') - transaction.commit_unless_managed() + if django.VERSION < (1, 6): + transaction.commit_unless_managed() if not found_missing_fields: print('No new translatable fields detected') @@ -102,7 +117,7 @@ def get_missing_languages(self, field_name, db_table): Gets only missings fields. """ db_table_fields = self.get_table_fields(db_table) - for lang_code, lang_name in settings.LANGUAGES: + for lang_code in AVAILABLE_LANGUAGES: if build_localized_fieldname(field_name, lang_code) not in db_table_fields: yield lang_code @@ -120,9 +135,8 @@ def get_sync_sql(self, field_name, missing_langs, model): col_type = f.db_type(connection=connection) field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)] # column creation - sql_output.append("ALTER TABLE %s ADD COLUMN %s;" % (qn(db_table), ' '.join(field_sql))) - if not f.null and lang == settings.LANGUAGE_CODE: - sql_output.append( - ("ALTER TABLE %s MODIFY COLUMN %s %s %s;" % ( - qn(db_table), qn(f.column), col_type, style.SQL_KEYWORD('NOT NULL')))) + stmt = "ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), ' '.join(field_sql)) + if not f.null: + stmt += " " + style.SQL_KEYWORD('NOT NULL') + sql_output.append(stmt + ";") return sql_output diff --git a/modeltranslation/management/commands/update_translation_fields.py b/modeltranslation/management/commands/update_translation_fields.py index 86f51ecb..98e1bfbe 100644 --- a/modeltranslation/management/commands/update_translation_fields.py +++ b/modeltranslation/management/commands/update_translation_fields.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- from django.db.models import F, Q -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from modeltranslation.settings import DEFAULT_LANGUAGE from modeltranslation.translator import translator from modeltranslation.utils import build_localized_fieldname -class Command(NoArgsCommand): +class Command(BaseCommand): help = ('Updates empty values of default translation fields using' ' values from original fields (in all translated models).') - def handle_noargs(self, **options): + def handle(self, *args, **options): verbosity = int(options['verbosity']) if verbosity > 0: self.stdout.write("Using default language: %s\n" % DEFAULT_LANGUAGE) @@ -29,5 +29,5 @@ def handle_noargs(self, **options): if field.empty_strings_allowed: q |= Q(**{def_lang_fieldname: ""}) - model.objects.filter(q).rewrite(False).update( + model._default_manager.filter(q).rewrite(False).update( **{def_lang_fieldname: F(field_name)}) diff --git a/modeltranslation/manager.py b/modeltranslation/manager.py index cf5922b3..4ad697c7 100644 --- a/modeltranslation/manager.py +++ b/modeltranslation/manager.py @@ -5,14 +5,44 @@ https://github.com/zmathew/django-linguo """ +import itertools + +import django +try: + from django.contrib.admin.utils import get_model_from_relation +except ImportError: + from django.contrib.admin.util import get_model_from_relation + from django.db import models -from django.db.models.fields.related import RelatedField, RelatedObject -from django.db.models.sql.where import Constraint +from django.db.models import FieldDoesNotExist +try: + from django.db.models.fields.related import RelatedObject + from django.db.models.fields.related import RelatedField + NEW_META_API = False +except ImportError: + NEW_META_API = True + +try: + from django.db.models.query import ValuesQuerySet + from django.db.models.sql.where import Constraint + NEW_RELATED_API = False +except ImportError: + from django.db.models.query import ValuesIterable + NEW_RELATED_API = True # Django 1.9 + +from django.utils.six import moves from django.utils.tree import Node +try: + from django.db.models.lookups import Lookup + from django.db.models.sql.datastructures import Col + NEW_LOOKUPS = True # Django 1.7, 1.8 +except ImportError: + NEW_LOOKUPS = False from modeltranslation import settings +from modeltranslation.fields import TranslationField from modeltranslation.utils import (build_localized_fieldname, get_language, - auto_populate) + auto_populate, resolution_order) def get_translatable_fields_for_model(model): @@ -24,40 +54,117 @@ def get_translatable_fields_for_model(model): def rewrite_lookup_key(model, lookup_key): + try: + pieces = lookup_key.split('__', 1) + original_key = pieces[0] + + translatable_fields = get_translatable_fields_for_model(model) + if translatable_fields is not None: + # If we are doing a lookup on a translatable field, + # we want to rewrite it to the actual field name + # For example, we want to rewrite "name__startswith" to "name_fr__startswith" + if pieces[0] in translatable_fields: + pieces[0] = build_localized_fieldname(pieces[0], get_language()) + + if len(pieces) > 1: + # Check if we are doing a lookup to a related trans model + fields_to_trans_models = get_fields_to_translatable_models(model) + # Check ``original key``, as pieces[0] may have been already rewritten. + if original_key in fields_to_trans_models: + transmodel = fields_to_trans_models[original_key] + pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) + return '__'.join(pieces) + except AttributeError: + return lookup_key + + +def append_fallback(model, fields): + """ + If translated field is encountered, add also all its fallback fields. + Returns tuple: (set_of_new_fields_to_use, set_of_translated_field_names) + """ + fields = set(fields) + trans = set() + from modeltranslation.translator import translator + opts = translator.get_options_for_model(model) + for key, _ in opts.fields.items(): + if key in fields: + langs = resolution_order(get_language(), getattr(model, key).fallback_languages) + fields = fields.union(build_localized_fieldname(key, lang) for lang in langs) + fields.remove(key) + trans.add(key) + return fields, trans + + +def append_translated(model, fields): + "If translated field is encountered, add also all its translation fields." + fields = set(fields) + from modeltranslation.translator import translator + opts = translator.get_options_for_model(model) + for key, translated in opts.fields.items(): + if key in fields: + fields = fields.union(f.name for f in translated) + return fields + + +def append_lookup_key(model, lookup_key): + "Transform spanned__lookup__key into all possible translation versions, on all levels" pieces = lookup_key.split('__', 1) - original_key = pieces[0] - translatable_fields = get_translatable_fields_for_model(model) - if translatable_fields is not None: - # If we are doing a lookup on a translatable field, - # we want to rewrite it to the actual field name - # For example, we want to rewrite "name__startswith" to "name_fr__startswith" - if pieces[0] in translatable_fields: - pieces[0] = build_localized_fieldname(pieces[0], get_language()) + fields = append_translated(model, (pieces[0],)) if len(pieces) > 1: # Check if we are doing a lookup to a related trans model fields_to_trans_models = get_fields_to_translatable_models(model) - for field_to_trans, transmodel in fields_to_trans_models: - # Check ``original key``, as pieces[0] may have been already rewritten. - if original_key == field_to_trans: - pieces[1] = rewrite_lookup_key(transmodel, pieces[1]) - break - return '__'.join(pieces) + if pieces[0] in fields_to_trans_models: + transmodel = fields_to_trans_models[pieces[0]] + rest = append_lookup_key(transmodel, pieces[1]) + fields = set('__'.join(pr) for pr in itertools.product(fields, rest)) + else: + fields = set('%s__%s' % (f, pieces[1]) for f in fields) + return fields + + +def append_lookup_keys(model, fields): + new_fields = [] + for field in fields: + try: + new_field = append_lookup_key(model, field) + except AttributeError: + new_field = (field,) + new_fields.append(new_field) + + return moves.reduce(set.union, new_fields, set()) def rewrite_order_lookup_key(model, lookup_key): - if lookup_key.startswith('-'): - return '-' + rewrite_lookup_key(model, lookup_key[1:]) - else: - return rewrite_lookup_key(model, lookup_key) + try: + if lookup_key.startswith('-'): + return '-' + rewrite_lookup_key(model, lookup_key[1:]) + else: + return rewrite_lookup_key(model, lookup_key) + except AttributeError: + return lookup_key _F2TM_CACHE = {} def get_fields_to_translatable_models(model): - if model not in _F2TM_CACHE: - results = [] + if model in _F2TM_CACHE: + return _F2TM_CACHE[model] + + results = [] + if NEW_META_API: + for f in model._meta.get_fields(): + if f.is_relation and f.related_model: + # The new get_field() will find GenericForeignKey relations. + # In that case the 'related_model' attribute is set to None + # so it is necessary to check for this value before trying to + # get translatable fields. + related_model = get_model_from_relation(f) + if get_translatable_fields_for_model(related_model) is not None: + results.append((f.name, related_model)) + else: for field_name in model._meta.get_all_field_names(): field_object, modelclass, direct, m2m = model._meta.get_field_by_name(field_name) # Direct relationship @@ -68,9 +175,30 @@ def get_fields_to_translatable_models(model): if isinstance(field_object, RelatedObject): if get_translatable_fields_for_model(field_object.model) is not None: results.append((field_name, field_object.model)) - _F2TM_CACHE[model] = results + _F2TM_CACHE[model] = dict(results) return _F2TM_CACHE[model] +_C2F_CACHE = {} + + +def get_field_by_colum_name(model, col): + # First, try field with the column name + try: + field = model._meta.get_field(col) + if field.column == col: + return field + except FieldDoesNotExist: + pass + field = _C2F_CACHE.get((model, col), None) + if field: + return field + # D'oh, need to search through all of them. + for field in model._meta.fields: + if field.column == col: + _C2F_CACHE[(model, col)] = field + return field + assert False, "No field found for column %s" % col + class MultilingualQuerySet(models.query.QuerySet): def __init__(self, *args, **kwargs): @@ -89,16 +217,31 @@ def _post_init(self): ordering.append(rewrite_order_lookup_key(self.model, key)) self.query.add_ordering(*ordering) + def __reduce__(self): + return multilingual_queryset_factory, (self.__class__.__bases__[0],), self.__getstate__() + # This method was not present in django-linguo - def _clone(self, klass=None, *args, **kwargs): - if klass is not None and not issubclass(klass, MultilingualQuerySet): - class NewClass(klass, MultilingualQuerySet): - pass - NewClass.__name__ = 'Multilingual%s' % klass.__name__ - klass = NewClass - kwargs.setdefault('_rewrite', self._rewrite) - kwargs.setdefault('_populate', self._populate) - return super(MultilingualQuerySet, self)._clone(klass, *args, **kwargs) + if NEW_RELATED_API: + def _clone(self, klass=None, **kwargs): + kwargs.setdefault('_rewrite', self._rewrite) + kwargs.setdefault('_populate', self._populate) + if hasattr(self, 'translation_fields'): + kwargs.setdefault('translation_fields', self.translation_fields) + if hasattr(self, 'fields_to_del'): + kwargs.setdefault('fields_to_del', self.fields_to_del) + if hasattr(self, 'original_fields'): + kwargs.setdefault('original_fields', self.original_fields) + return super(MultilingualQuerySet, self)._clone(**kwargs) + else: + def _clone(self, klass=None, *args, **kwargs): + if klass is not None and not issubclass(klass, MultilingualQuerySet): + class NewClass(klass, MultilingualQuerySet): + pass + NewClass.__name__ = 'Multilingual%s' % klass.__name__ + klass = NewClass + kwargs.setdefault('_rewrite', self._rewrite) + kwargs.setdefault('_populate', self._populate) + return super(MultilingualQuerySet, self)._clone(klass, *args, **kwargs) # This method was not present in django-linguo def rewrite(self, mode=True): @@ -117,19 +260,56 @@ def _rewrite_applied_operations(self): Useful when converting any QuerySet into MultilingualQuerySet. """ self._rewrite_where(self.query.where) - self._rewrite_where(self.query.having) + if not NEW_RELATED_API: + self._rewrite_where(self.query.having) self._rewrite_order() + self._rewrite_select_related() + + # This method was not present in django-linguo + def select_related(self, *fields, **kwargs): + if not self._rewrite: + return super(MultilingualQuerySet, self).select_related(*fields, **kwargs) + # TO CONSIDER: whether this should rewrite only current language, or all languages? + # fk -> [fk, fk_en] (with en=active) VS fk -> [fk, fk_en, fk_de, fk_fr ...] (for all langs) + + # new_args = append_lookup_keys(self.model, fields) + new_args = [] + for key in fields: + if key is None: + new_args.append(None) + else: + new_args.append(rewrite_lookup_key(self.model, key)) + return super(MultilingualQuerySet, self).select_related(*new_args, **kwargs) + + # This method was not present in django-linguo + def _rewrite_col(self, col): + """Django >= 1.7 column name rewriting""" + if isinstance(col, Col): + new_name = rewrite_lookup_key(self.model, col.target.name) + if col.target.name != new_name: + new_field = self.model._meta.get_field(new_name) + if col.target is col.source: + col.source = new_field + col.target = new_field + elif hasattr(col, 'col'): + self._rewrite_col(col.col) + elif hasattr(col, 'lhs'): + self._rewrite_col(col.lhs) def _rewrite_where(self, q): """ Rewrite field names inside WHERE tree. """ - if isinstance(q, tuple) and isinstance(q[0], Constraint): + if not NEW_LOOKUPS and isinstance(q, tuple) and isinstance(q[0], Constraint): c = q[0] + if c.field is None: + c.field = get_field_by_colum_name(self.model, c.col) new_name = rewrite_lookup_key(self.model, c.field.name) if c.field.name != new_name: c.field = self.model._meta.get_field(new_name) c.col = c.field.column + elif NEW_LOOKUPS and isinstance(q, Lookup): + self._rewrite_col(q.lhs) if isinstance(q, Node): for child in q.children: self._rewrite_where(child) @@ -138,6 +318,13 @@ def _rewrite_order(self): self.query.order_by = [rewrite_order_lookup_key(self.model, field_name) for field_name in self.query.order_by] + def _rewrite_select_related(self): + if isinstance(self.query.select_related, dict): + new = {} + for field_name, value in self.query.select_related.items(): + new[rewrite_order_lookup_key(self.model, field_name)] = value + self.query.select_related = new + # This method was not present in django-linguo def _rewrite_q(self, q): """Rewrite field names inside Q call.""" @@ -157,18 +344,28 @@ def _rewrite_f(self, q): return q if isinstance(q, Node): q.children = list(map(self._rewrite_f, q.children)) + # Django >= 1.8 + if hasattr(q, 'lhs'): + q.lhs = self._rewrite_f(q.lhs) + if hasattr(q, 'rhs'): + q.rhs = self._rewrite_f(q.rhs) return q def _filter_or_exclude(self, negate, *args, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) args = map(self._rewrite_q, args) - for key, val in kwargs.items(): + for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) return super(MultilingualQuerySet, self)._filter_or_exclude(negate, *args, **kwargs) + def _get_original_fields(self): + source = (self.model._meta.concrete_fields if hasattr(self.model._meta, 'concrete_fields') + else self.model._meta.fields) + return [f.attname for f in source if not isinstance(f, TranslationField)] + def order_by(self, *field_names): """ Change translatable field names in an ``order_by`` argument @@ -184,7 +381,7 @@ def order_by(self, *field_names): def update(self, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).update(**kwargs) - for key, val in kwargs.items(): + for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(self.model, key) del kwargs[key] kwargs[new_key] = self._rewrite_f(val) @@ -215,49 +412,65 @@ def get_or_create(self, **kwargs): with auto_populate(self._populate_mode): return super(MultilingualQuerySet, self).get_or_create(**kwargs) - def _append_translated(self, fields): - "If translated field is encountered, add also all its translation fields." - fields = set(fields) - from modeltranslation.translator import translator - opts = translator.get_options_for_model(self.model) - for key, translated in opts.fields.items(): - if key in fields: - fields = fields.union(f.name for f in translated) - return fields - # This method was not present in django-linguo def defer(self, *fields): - fields = self._append_translated(fields) + fields = append_lookup_keys(self.model, fields) return super(MultilingualQuerySet, self).defer(*fields) # This method was not present in django-linguo def only(self, *fields): - fields = self._append_translated(fields) + fields = append_lookup_keys(self.model, fields) return super(MultilingualQuerySet, self).only(*fields) # This method was not present in django-linguo def raw_values(self, *fields): return super(MultilingualQuerySet, self).values(*fields) + def _values(self, *original, **kwargs): + if not kwargs.get('prepare', False): + return super(MultilingualQuerySet, self)._values(*original) + new_fields, translation_fields = append_fallback(self.model, original) + clone = super(MultilingualQuerySet, self)._values(*list(new_fields)) + clone.original_fields = tuple(original) + clone.translation_fields = translation_fields + clone.fields_to_del = new_fields - set(original) + return clone + # This method was not present in django-linguo def values(self, *fields): if not self._rewrite: return super(MultilingualQuerySet, self).values(*fields) - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - vqs = super(MultilingualQuerySet, self).values(*new_args) - vqs.field_names = list(fields) - return vqs + if not fields: + # Emulate original queryset behaviour: get all fields that are not translation fields + fields = self._get_original_fields() + if NEW_RELATED_API: + clone = self._values(*fields, prepare=True) + clone._iterable_class = FallbackValuesIterable + return clone + else: + return self._clone(klass=FallbackValuesQuerySet, setup=True, _fields=fields) # This method was not present in django-linguo def values_list(self, *fields, **kwargs): if not self._rewrite: return super(MultilingualQuerySet, self).values_list(*fields, **kwargs) - new_args = [] - for key in fields: - new_args.append(rewrite_lookup_key(self.model, key)) - return super(MultilingualQuerySet, self).values_list(*new_args, **kwargs) + flat = kwargs.pop('flat', False) + if kwargs: + raise TypeError('Unexpected keyword arguments to values_list: %s' % (list(kwargs),)) + if flat and len(fields) > 1: + raise TypeError("'flat' is not valid when values_list is " + "called with more than one field.") + if not fields: + # Emulate original queryset behaviour: get all fields that are not translation fields + fields = self._get_original_fields() + if NEW_RELATED_API: + clone = self._values(*fields, prepare=True) + clone._iterable_class = (FallbackFlatValuesListIterable if flat + else FallbackValuesListIterable) + return clone + else: + return self._clone(klass=FallbackValuesListQuerySet, setup=True, flat=flat, + _fields=fields) # This method was not present in django-linguo def dates(self, field_name, *args, **kwargs): @@ -267,27 +480,140 @@ def dates(self, field_name, *args, **kwargs): return super(MultilingualQuerySet, self).dates(new_key, *args, **kwargs) -class MultilingualManager(models.Manager): - use_for_related_fields = True +if NEW_RELATED_API: + class FallbackValuesIterable(ValuesIterable): + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass + + def __iter__(self): + instance = self.X() + for row in super(FallbackValuesIterable, self).__iter__(): + instance.__dict__.update(row) + for key in self.queryset.translation_fields: + row[key] = getattr(self.queryset.model, key).__get__(instance, None) + for key in self.queryset.fields_to_del: + del row[key] + yield row + + class FallbackValuesListIterable(FallbackValuesIterable): + def __iter__(self): + fields = self.queryset.original_fields + fields += tuple(f for f in self.queryset.query.annotation_select if f not in fields) + for row in super(FallbackValuesListIterable, self).__iter__(): + yield tuple(row[f] for f in fields) + + class FallbackFlatValuesListIterable(FallbackValuesListIterable): + def __iter__(self): + for row in super(FallbackFlatValuesListIterable, self).__iter__(): + yield row[0] + +else: + class FallbackValuesQuerySet(ValuesQuerySet, MultilingualQuerySet): + def _setup_query(self): + original = self._fields + new_fields, self.translation_fields = append_fallback(self.model, original) + self._fields = list(new_fields) + self.fields_to_del = new_fields - set(original) + super(FallbackValuesQuerySet, self)._setup_query() + + class X(object): + # This stupid class is needed as object use __slots__ and has no __dict__. + pass + + def iterator(self): + instance = self.X() + for row in super(FallbackValuesQuerySet, self).iterator(): + instance.__dict__.update(row) + for key in self.translation_fields: + row[key] = getattr(self.model, key).__get__(instance, None) + for key in self.fields_to_del: + del row[key] + yield row + + def _clone(self, klass=None, setup=False, **kwargs): + c = super(FallbackValuesQuerySet, self)._clone(klass, **kwargs) + c.fields_to_del = self.fields_to_del + c.translation_fields = self.translation_fields + if setup and hasattr(c, '_setup_query'): + c._setup_query() + return c + + class FallbackValuesListQuerySet(FallbackValuesQuerySet): + def iterator(self): + fields = self.original_fields + if hasattr(self, 'aggregate_names'): + # Django <1.8 + fields += tuple(f for f in self.aggregate_names if f not in fields) + if hasattr(self, 'annotation_names'): + # Django >=1.8 + fields += tuple(f for f in self.annotation_names if f not in fields) + for row in super(FallbackValuesListQuerySet, self).iterator(): + if self.flat and len(self.original_fields) == 1: + yield row[fields[0]] + else: + yield tuple(row[f] for f in fields) + + def _setup_query(self): + self.original_fields = tuple(self._fields) + super(FallbackValuesListQuerySet, self)._setup_query() + + def _clone(self, *args, **kwargs): + clone = super(FallbackValuesListQuerySet, self)._clone(*args, **kwargs) + clone.original_fields = self.original_fields + if not hasattr(clone, "flat"): + # Only assign flat if the clone didn't already get it from kwargs + clone.flat = self.flat + return clone + + +def multilingual_queryset_factory(old_cls, instantiate=True): + if old_cls == models.query.QuerySet: + NewClass = MultilingualQuerySet + else: + class NewClass(old_cls, MultilingualQuerySet): + pass + NewClass.__name__ = 'Multilingual%s' % old_cls.__name__ + return NewClass() if instantiate else NewClass + + +class MultilingualQuerysetManager(models.Manager): + """ + This class gets hooked in MRO just before plain Manager, so that every call to + get_queryset returns MultilingualQuerySet. + """ + def get_queryset(self): + qs = super(MultilingualQuerysetManager, self).get_queryset() + return self._patch_queryset(qs) + + def _patch_queryset(self, qs): + qs.__class__ = multilingual_queryset_factory(qs.__class__, instantiate=False) + qs._post_init() + qs._rewrite_applied_operations() + return qs + + +class MultilingualManager(MultilingualQuerysetManager): + if django.VERSION < (1, 10): + use_for_related_fields = True def rewrite(self, *args, **kwargs): - return self.get_query_set().rewrite(*args, **kwargs) + return self.get_queryset().rewrite(*args, **kwargs) def populate(self, *args, **kwargs): - return self.get_query_set().populate(*args, **kwargs) + return self.get_queryset().populate(*args, **kwargs) def raw_values(self, *args, **kwargs): - return self.get_query_set().raw_values(*args, **kwargs) + return self.get_queryset().raw_values(*args, **kwargs) - def get_query_set(self): - qs = super(MultilingualManager, self).get_query_set() - if qs.__class__ == models.query.QuerySet: - qs.__class__ = MultilingualQuerySet - else: - class NewClass(qs.__class__, MultilingualQuerySet): - pass - NewClass.__name__ = 'Multilingual%s' % qs.__class__.__name__ - qs.__class__ = NewClass - qs._post_init() - qs._rewrite_applied_operations() - return qs + def get_queryset(self): + """ + This method is repeated because some managers that don't use super() or alter queryset class + may return queryset that is not subclass of MultilingualQuerySet. + """ + qs = super(MultilingualManager, self).get_queryset() + if isinstance(qs, MultilingualQuerySet): + # Is already patched by MultilingualQuerysetManager - in most of the cases + # when custom managers use super() properly in get_queryset. + return qs + return self._patch_queryset(qs) diff --git a/modeltranslation/models.py b/modeltranslation/models.py index 72c5e9cf..bc018bb4 100644 --- a/modeltranslation/models.py +++ b/modeltranslation/models.py @@ -10,14 +10,15 @@ def autodiscover(): import os import sys import copy - from django.conf import settings - from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule from modeltranslation.translator import translator from modeltranslation.settings import TRANSLATION_FILES, DEBUG - for app in settings.INSTALLED_APPS: - mod = import_module(app) + from importlib import import_module + from django.apps import apps + mods = [(app_config.name, app_config.module) for app_config in apps.get_app_configs()] + + for (app, mod) in mods: # Attempt to import the app's translation module. module = '%s.translation' % app before_import_registry = copy.copy(translator._registry) @@ -72,6 +73,3 @@ def handle_translation_registrations(*args, **kwargs): # Trigger autodiscover, causing any TranslationOption initialization # code to execute. autodiscover() - - -handle_translation_registrations() diff --git a/modeltranslation/settings.py b/modeltranslation/settings.py index 68b3997b..309075d7 100644 --- a/modeltranslation/settings.py +++ b/modeltranslation/settings.py @@ -5,7 +5,8 @@ TRANSLATION_FILES = tuple(getattr(settings, 'MODELTRANSLATION_TRANSLATION_FILES', ())) -AVAILABLE_LANGUAGES = [l[0] for l in settings.LANGUAGES] +AVAILABLE_LANGUAGES = list(getattr(settings, 'MODELTRANSLATION_LANGUAGES', + (l[0] for l in settings.LANGUAGES))) DEFAULT_LANGUAGE = getattr(settings, 'MODELTRANSLATION_DEFAULT_LANGUAGE', None) if DEFAULT_LANGUAGE and DEFAULT_LANGUAGE not in AVAILABLE_LANGUAGES: raise ImproperlyConfigured('MODELTRANSLATION_DEFAULT_LANGUAGE not in LANGUAGES setting.') @@ -35,7 +36,7 @@ # By default we fallback to the default language FALLBACK_LANGUAGES = getattr(settings, 'MODELTRANSLATION_FALLBACK_LANGUAGES', (DEFAULT_LANGUAGE,)) if isinstance(FALLBACK_LANGUAGES, (tuple, list)): - FALLBACK_LANGUAGES = {'default': FALLBACK_LANGUAGES} + FALLBACK_LANGUAGES = {'default': tuple(FALLBACK_LANGUAGES)} if 'default' not in FALLBACK_LANGUAGES: raise ImproperlyConfigured( 'MODELTRANSLATION_FALLBACK_LANGUAGES does not contain "default" key.') diff --git a/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css b/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css index 102fef9d..d21fe7d4 100644 --- a/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css +++ b/modeltranslation/static/modeltranslation/css/tabbed_translation_fields.css @@ -36,7 +36,7 @@ backward compatibility: .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } .ui-tabs .ui-tabs-hide { position: absolute; - left: -10000px; + display: none; } /* custom tabs theme */ diff --git a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js index 50d7a6e9..0b3c4de5 100644 --- a/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js +++ b/modeltranslation/static/modeltranslation/js/tabbed_translation_fields.js @@ -3,7 +3,8 @@ var google, django, gettext; (function () { - var jQuery = jQuery || $ || django.jQuery; + var jQuery = window.jQuery || $ || django.jQuery; + /* Add a new selector to jQuery that excludes parent items which match a given selector */ jQuery.expr[':'].parents = function(a, i, m) { return jQuery(a).parents(m[3]).length < 1; @@ -186,7 +187,10 @@ var google, django, gettext; // TODO: Refactor $('.mt').parents('.inline-group').not('.tabular').find('.add-row a').click(function () { var grouper = new TranslationFieldGrouper({ - $fields: $(this).parent().prev().prev().find('.mt') + $fields: $(this).parent().prev().prev().find('.mt').add( + // Support django-nested-admin stacked inlines + $(this).parent().prev('.djn-items').children('.djn-item').last().find('.mt') + ) }); var tabs = createTabs(grouper.groupedTranslations); // Update the main switch as it is not aware of the newly created tabs @@ -212,7 +216,7 @@ var google, django, gettext; this.getAllGroupedTranslations = function () { var grouper = new TranslationFieldGrouper({ $fields: this.$table.find('.mt').filter( - 'input:visible, textarea:visible, select:visible') + 'input, textarea, select') }); //this.requiredColumns = this.getRequiredColumns(); this.initTable(); @@ -391,7 +395,7 @@ var google, django, gettext; // Group normal fields and fields in (existing) stacked inlines var grouper = new TranslationFieldGrouper({ $fields: $('.mt').filter( - 'input:visible, textarea:visible, select:visible').filter(':parents(.tabular)') + 'input:visible, textarea:visible, select:visible, iframe, div').filter(':parents(.tabular)') }); MainSwitch.init(grouper.groupedTranslations, createTabs(grouper.groupedTranslations)); diff --git a/modeltranslation/tests/__init__.py b/modeltranslation/tests/__init__.py index ef59e845..e69de29b 100644 --- a/modeltranslation/tests/__init__.py +++ b/modeltranslation/tests/__init__.py @@ -1,2711 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from decimal import Decimal -import os -import shutil -import imp - -from django import forms -from django.conf import settings as django_settings -from django.contrib.admin.sites import AdminSite -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError, ImproperlyConfigured -from django.core.files.base import ContentFile -from django.core.files.storage import default_storage -from django.core.management import call_command -from django.db import IntegrityError -from django.db.models import Q, F -from django.db.models.loading import AppCache -from django.test import TestCase, TransactionTestCase -from django.test.utils import override_settings -from django.utils import six -from django.utils.translation import get_language, override, trans_real - -from modeltranslation import admin, settings as mt_settings, translator -from modeltranslation.forms import TranslationModelForm -from modeltranslation.models import autodiscover -from modeltranslation.tests import models -from modeltranslation.tests.translation import (FallbackModel2TranslationOptions, - FieldInheritanceCTranslationOptions, - FieldInheritanceETranslationOptions) -from modeltranslation.tests.test_settings import TEST_SETTINGS -from modeltranslation.utils import (build_css_class, build_localized_fieldname, - auto_populate, fallbacks) - - -# None of the following tests really depend on the content of the request, -# so we'll just pass in None. -request = None - -# How many models are registered for tests. -TEST_MODELS = 27 - - -class reload_override_settings(override_settings): - """Context manager that not only override settings, but also reload modeltranslation conf.""" - def __enter__(self): - super(reload_override_settings, self).__enter__() - imp.reload(mt_settings) - - def __exit__(self, exc_type, exc_value, traceback): - super(reload_override_settings, self).__exit__(exc_type, exc_value, traceback) - imp.reload(mt_settings) - - -# In this test suite fallback language is turned off. This context manager temporarily turns it on. -def default_fallback(): - return reload_override_settings( - MODELTRANSLATION_FALLBACK_LANGUAGES=(mt_settings.DEFAULT_LANGUAGE,)) - - -@override_settings(**TEST_SETTINGS) -class ModeltranslationTransactionTestBase(TransactionTestCase): - urls = 'modeltranslation.tests.urls' - cache = AppCache() - synced = False - - @classmethod - def setUpClass(cls): - """ - Prepare database: - * Call syncdb to create tables for tests.models (since during - default testrunner's db creation modeltranslation.tests was not in INSTALLED_APPS - """ - super(ModeltranslationTransactionTestBase, cls).setUpClass() - if not ModeltranslationTestBase.synced: - # In order to perform only one syncdb - ModeltranslationTestBase.synced = True - with override_settings(**TEST_SETTINGS): - import sys - - # 1. Reload translation in case USE_I18N was False - from django.utils import translation - imp.reload(translation) - - # 2. Reload MT because LANGUAGES likely changed. - imp.reload(mt_settings) - imp.reload(translator) - imp.reload(admin) - - # 3. Reset test models (because autodiscover have already run, those models - # have translation fields, but for languages previously defined. We want - # to be sure that 'de' and 'en' are available) - del cls.cache.app_models['tests'] - imp.reload(models) - cls.cache.load_app('modeltranslation.tests') - sys.modules.pop('modeltranslation.tests.translation', None) - - # 4. Autodiscover - from modeltranslation import models as aut_models - imp.reload(aut_models) - - # 5. Syncdb (``migrate=False`` in case of south) - from django.db import connections, DEFAULT_DB_ALIAS - call_command('syncdb', verbosity=0, migrate=False, interactive=False, - database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) - - def setUp(self): - self._old_language = get_language() - trans_real.activate('de') - - def tearDown(self): - trans_real.activate(self._old_language) - - -class ModeltranslationTestBase(ModeltranslationTransactionTestBase, TestCase): - pass - - -class TestAutodiscover(ModeltranslationTestBase): - # The way the ``override_settings`` works on ``TestCase`` is wicked; - # it patches ``_pre_setup`` and ``_post_teardown`` methods. - # Because of this, if class B extends class A and both are ``override_settings``'ed, - # class B settings would be overwritten by class A settings (if some keys clash). - # To solve this, override some settings after parents ``_pre_setup`` is called. - def _pre_setup(self): - super(TestAutodiscover, self)._pre_setup() - # Add test_app to INSTALLED_APPS - new_installed_apps = django_settings.INSTALLED_APPS + ('modeltranslation.tests.test_app',) - self.__override = override_settings(INSTALLED_APPS=new_installed_apps) - self.__override.enable() - - def _post_teardown(self): - self.__override.disable() - imp.reload(mt_settings) # restore mt_settings.FALLBACK_LANGUAGES - super(TestAutodiscover, self)._post_teardown() - - @classmethod - def setUpClass(cls): - """Save registry (and restore it after tests).""" - super(TestAutodiscover, cls).setUpClass() - from copy import copy - from modeltranslation.translator import translator - cls.registry_cpy = copy(translator._registry) - - @classmethod - def tearDownClass(cls): - from modeltranslation.translator import translator - translator._registry = cls.registry_cpy - super(TestAutodiscover, cls).tearDownClass() - - def tearDown(self): - import sys - # Rollback model classes - del self.cache.app_models['test_app'] - from .test_app import models - imp.reload(models) - # Delete translation modules from import cache - sys.modules.pop('modeltranslation.tests.test_app.translation', None) - sys.modules.pop('modeltranslation.tests.project_translation', None) - super(TestAutodiscover, self).tearDown() - - def check_news(self): - from .test_app.models import News - fields = dir(News()) - self.assertIn('title', fields) - self.assertIn('title_en', fields) - self.assertIn('title_de', fields) - self.assertIn('visits', fields) - self.assertNotIn('visits_en', fields) - self.assertNotIn('visits_de', fields) - - def check_other(self, present=True): - from .test_app.models import Other - fields = dir(Other()) - self.assertIn('name', fields) - if present: - self.assertIn('name_en', fields) - self.assertIn('name_de', fields) - else: - self.assertNotIn('name_en', fields) - self.assertNotIn('name_de', fields) - - def test_simple(self): - """Check if translation is imported for installed apps.""" - autodiscover() - self.check_news() - self.check_other(present=False) - - @reload_override_settings( - MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.project_translation',) - ) - def test_global(self): - """Check if translation is imported for global translation file.""" - autodiscover() - self.check_news() - self.check_other() - - @reload_override_settings( - MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.test_app.translation',) - ) - def test_duplication(self): - """Check if there is no problem with duplicated filenames.""" - autodiscover() - self.check_news() - - -class ModeltranslationTest(ModeltranslationTestBase): - """Basic tests for the modeltranslation application.""" - def test_registration(self): - langs = tuple(l[0] for l in django_settings.LANGUAGES) - self.assertEqual(langs, tuple(mt_settings.AVAILABLE_LANGUAGES)) - self.assertEqual(2, len(langs)) - self.assertTrue('de' in langs) - self.assertTrue('en' in langs) - self.assertTrue(translator.translator) - - # Check that all models are registered for translation - self.assertEqual(len(translator.translator.get_registered_models()), TEST_MODELS) - - # Try to unregister a model that is not registered - self.assertRaises(translator.NotRegistered, - translator.translator.unregister, models.BasePage) - - # Try to get options for a model that is not registered - self.assertRaises(translator.NotRegistered, - translator.translator.get_options_for_model, User) - - # Ensure that a base can't be registered after a subclass. - self.assertRaises(translator.DescendantRegistered, - translator.translator.register, models.BasePage) - - # Or unregistered before it. - self.assertRaises(translator.DescendantRegistered, - translator.translator.unregister, models.Slugged) - - def test_fields(self): - field_names = dir(models.TestModel()) - self.assertTrue('id' in field_names) - self.assertTrue('title' in field_names) - self.assertTrue('title_de' in field_names) - self.assertTrue('title_en' in field_names) - self.assertTrue('text' in field_names) - self.assertTrue('text_de' in field_names) - self.assertTrue('text_en' in field_names) - self.assertTrue('url' in field_names) - self.assertTrue('url_de' in field_names) - self.assertTrue('url_en' in field_names) - self.assertTrue('email' in field_names) - self.assertTrue('email_de' in field_names) - self.assertTrue('email_en' in field_names) - - def test_verbose_name(self): - verbose_name = models.TestModel._meta.get_field('title_de').verbose_name - self.assertEqual(six.text_type(verbose_name), 'title [de]') - - def test_descriptor_introspection(self): - # See Django #8248 - try: - models.TestModel.title - models.TestModel.title.__doc__ - self.assertTrue(True) - except: - self.fail('Descriptor accessed on class should return itself.') - - def test_fields_hashes(self): - opts = models.TestModel._meta - orig = opts.get_field('title') - en = opts.get_field('title_en') - de = opts.get_field('title_de') - # Translation field retain creation_counters - self.assertEqual(orig.creation_counter, en.creation_counter) - self.assertEqual(orig.creation_counter, de.creation_counter) - # But they compare unequal - self.assertNotEqual(orig, en) - self.assertNotEqual(orig, de) - self.assertNotEqual(en, de) - # Their hashes too - self.assertNotEqual(hash(orig), hash(en)) - self.assertNotEqual(hash(orig), hash(de)) - self.assertNotEqual(hash(en), hash(de)) - self.assertEqual(3, len(set([orig, en, de]))) - # TranslationFields can compare equal if they have the same language - de.language = 'en' - self.assertNotEqual(orig, de) - self.assertEqual(en, de) - self.assertEqual(hash(en), hash(de)) - self.assertEqual(2, len(set([orig, en, de]))) - de.language = 'de' - - def test_set_translation(self): - """This test briefly shows main modeltranslation features.""" - self.assertEqual(get_language(), 'de') - title_de = "title de" - title_en = "title en" - - # The original field "title" passed in the constructor is - # populated for the current language field: "title_de". - inst2 = models.TestModel(title=title_de) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) - self.assertEqual(inst2.title_de, title_de) - - # So creating object is language-aware - with override('en'): - inst2 = models.TestModel(title=title_en) - self.assertEqual(inst2.title, title_en) - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, None) - - # Value from original field is presented in current language: - inst2 = models.TestModel(title_de=title_de, title_en=title_en) - self.assertEqual(inst2.title, title_de) - with override('en'): - self.assertEqual(inst2.title, title_en) - - # Changes made via original field affect current language field: - inst2.title = 'foo' - self.assertEqual(inst2.title, 'foo') - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, 'foo') - with override('en'): - inst2.title = 'bar' - self.assertEqual(inst2.title, 'bar') - self.assertEqual(inst2.title_en, 'bar') - self.assertEqual(inst2.title_de, 'foo') - self.assertEqual(inst2.title, 'foo') - - # When conflict, language field wins with original field - inst2 = models.TestModel(title='foo', title_de=title_de, title_en=title_en) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, title_en) - self.assertEqual(inst2.title_de, title_de) - - # Creating model and assigning only one language - inst1 = models.TestModel(title_en=title_en) - # Please note: '' and not None, because descriptor falls back to field default value - self.assertEqual(inst1.title, '') - self.assertEqual(inst1.title_en, title_en) - self.assertEqual(inst1.title_de, None) - # Assign current language value - de - inst1.title = title_de - self.assertEqual(inst1.title, title_de) - self.assertEqual(inst1.title_en, title_en) - self.assertEqual(inst1.title_de, title_de) - inst1.save() - - # Check that the translation fields are correctly saved and provide the - # correct value when retrieving them again. - n = models.TestModel.objects.get(title=title_de) - self.assertEqual(n.title, title_de) - self.assertEqual(n.title_en, title_en) - self.assertEqual(n.title_de, title_de) - - # Queries are also language-aware: - self.assertEqual(1, models.TestModel.objects.filter(title=title_de).count()) - with override('en'): - self.assertEqual(0, models.TestModel.objects.filter(title=title_de).count()) - - def test_fallback_language(self): - # Present what happens if current language field is empty - self.assertEqual(get_language(), 'de') - title_de = "title de" - - # Create model with value in de only... - inst2 = models.TestModel(title=title_de) - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) - self.assertEqual(inst2.title_de, title_de) - - # In this test environment, fallback language is not set. So return value for en - # will be field's default: '' - with override('en'): - self.assertEqual(inst2.title, '') - self.assertEqual(inst2.title_en, None) # Language field access returns real value - - # However, by default FALLBACK_LANGUAGES is set to DEFAULT_LANGUAGE - with default_fallback(): - - # No change here... - self.assertEqual(inst2.title, title_de) - - # ... but for empty en fall back to de - with override('en'): - self.assertEqual(inst2.title, title_de) - self.assertEqual(inst2.title_en, None) # Still real value - - def test_fallback_values_1(self): - """ - If ``fallback_values`` is set to string, all untranslated fields would - return this string. - """ - title1_de = "title de" - n = models.FallbackModel(title=title1_de) - n.save() - n = models.FallbackModel.objects.get(title=title1_de) - self.assertEqual(n.title, title1_de) - trans_real.activate("en") - self.assertEqual(n.title, "fallback") - - def test_fallback_values_2(self): - """ - If ``fallback_values`` is set to ``dict``, all untranslated fields in - ``dict`` would return this mapped value. Fields not in ``dict`` would - return default translation. - """ - title1_de = "title de" - text1_de = "text in german" - n = models.FallbackModel2(title=title1_de, text=text1_de) - n.save() - n = models.FallbackModel2.objects.get(title=title1_de) - trans_real.activate("en") - self.assertEqual(n.title, '') # Falling back to default field value - self.assertEqual( - n.text, - FallbackModel2TranslationOptions.fallback_values['text']) - - def _compare_instances(self, x, y, field): - self.assertEqual(getattr(x, field), getattr(y, field), - "Constructor diff on field %s." % field) - - def _test_constructor(self, keywords): - n = models.TestModel(**keywords) - m = models.TestModel.objects.create(**keywords) - opts = translator.translator.get_options_for_model(models.TestModel) - for base_field, trans_fields in opts.fields.items(): - self._compare_instances(n, m, base_field) - for lang_field in trans_fields: - self._compare_instances(n, m, lang_field.name) - - def test_constructor(self): - """ - Ensure that model constructor behaves exactly the same as objects.create - """ - # test different arguments compositions - keywords = dict( - # original only - title='title', - # both languages + original - email='q@q.qq', email_de='d@d.dd', email_en='e@e.ee', - # both languages without original - text_en='text en', text_de='text de', - ) - self._test_constructor(keywords) - - keywords = dict( - # only current language - title_de='title', - # only not current language - url_en='http://www.google.com', - # original + current - text='text def', text_de='text de', - # original + not current - email='q@q.qq', email_en='e@e.ee', - ) - self._test_constructor(keywords) - - -class ModeltranslationTransactionTest(ModeltranslationTransactionTestBase): - def test_unique_nullable_field(self): - from django.db import transaction - models.UniqueNullableModel.objects.create() - models.UniqueNullableModel.objects.create() - models.UniqueNullableModel.objects.create(title=None) - models.UniqueNullableModel.objects.create(title=None) - - models.UniqueNullableModel.objects.create(title='') - self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='') - transaction.rollback() # Postgres - models.UniqueNullableModel.objects.create(title='foo') - self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='foo') - transaction.rollback() # Postgres - - -class FallbackTests(ModeltranslationTestBase): - test_fallback = { - 'default': ('de',), - 'de': ('en',) - } - - def test_settings(self): - # Initial - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ()}) - # Tuple/list - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('de',)): - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ('de',)}) - # Whole dict - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - self.assertEqual(mt_settings.FALLBACK_LANGUAGES, self.test_fallback) - # Improper language raises error - config = {'default': (), 'fr': ('en',)} - with override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=config): - self.assertRaises(ImproperlyConfigured, lambda: imp.reload(mt_settings)) - imp.reload(mt_settings) - - def test_resolution_order(self): - from modeltranslation.utils import resolution_order - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - self.assertEqual(('en', 'de'), resolution_order('en')) - self.assertEqual(('de', 'en'), resolution_order('de')) - # Overriding - config = {'default': ()} - self.assertEqual(('en',), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - # Uniqueness - config = {'de': ('en', 'de')} - self.assertEqual(('en', 'de'), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - - # Default fallbacks are always used at the end - # That's it: fallbacks specified for a language don't replace defaults, - # but just are prepended - config = {'default': ('en', 'de'), 'de': ()} - self.assertEqual(('en', 'de'), resolution_order('en', config)) - self.assertEqual(('de', 'en'), resolution_order('de', config)) - # What one may have expected - self.assertNotEqual(('de',), resolution_order('de', config)) - - # To completely override settings, one should override all keys - config = {'default': (), 'de': ()} - self.assertEqual(('en',), resolution_order('en', config)) - self.assertEqual(('de',), resolution_order('de', config)) - - def test_fallback_languages(self): - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - title_de = 'title de' - title_en = 'title en' - n = models.TestModel(title=title_de) - self.assertEqual(n.title_de, title_de) - self.assertEqual(n.title_en, None) - self.assertEqual(n.title, title_de) - trans_real.activate('en') - self.assertEqual(n.title, title_de) # since default fallback is de - - n = models.TestModel(title=title_en) - self.assertEqual(n.title_de, None) - self.assertEqual(n.title_en, title_en) - self.assertEqual(n.title, title_en) - trans_real.activate('de') - self.assertEqual(n.title, title_en) # since fallback for de is en - - n.title_en = None - self.assertEqual(n.title, '') # if all fallbacks fail, return field.get_default() - - def test_fallbacks_toggle(self): - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - m = models.TestModel(title='foo') - with fallbacks(True): - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.title_en, None) - self.assertEqual(m.title, 'foo') - with override('en'): - self.assertEqual(m.title, 'foo') - with fallbacks(False): - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.title_en, None) - self.assertEqual(m.title, 'foo') - with override('en'): - self.assertEqual(m.title, '') # '' is the default - - def test_fallback_undefined(self): - """ - Checks if a sensible value is considered undefined and triggers - fallbacks. Tests if the value can be overridden as documented. - """ - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): - # Non-nullable CharField falls back on empty strings. - m = models.FallbackModel(title_en='value', title_de='') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, 'value') - - # Nullable CharField does not fall back on empty strings. - m = models.FallbackModel(description_en='value', description_de='') - with override('en'): - self.assertEqual(m.description, 'value') - with override('de'): - self.assertEqual(m.description, '') - - # Nullable CharField does fall back on None. - m = models.FallbackModel(description_en='value', description_de=None) - with override('en'): - self.assertEqual(m.description, 'value') - with override('de'): - self.assertEqual(m.description, 'value') - - # The undefined value may be overridden. - m = models.FallbackModel2(title_en='value', title_de='') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, '') - m = models.FallbackModel2(title_en='value', title_de='no title') - with override('en'): - self.assertEqual(m.title, 'value') - with override('de'): - self.assertEqual(m.title, 'value') - - -class FileFieldsTest(ModeltranslationTestBase): - - def tearDown(self): - if default_storage.exists('modeltranslation_tests'): - # With FileSystemStorage uploading files creates a new directory, - # that's not automatically removed upon their deletion. - tests_dir = default_storage.path('modeltranslation_tests') - if os.path.isdir(tests_dir): - shutil.rmtree(tests_dir) - super(FileFieldsTest, self).tearDown() - - def test_translated_models(self): - field_names = dir(models.FileFieldsModel()) - self.assertTrue('id' in field_names) - self.assertTrue('title' in field_names) - self.assertTrue('title_de' in field_names) - self.assertTrue('title_en' in field_names) - self.assertTrue('file' in field_names) - self.assertTrue('file_de' in field_names) - self.assertTrue('file_en' in field_names) - self.assertTrue('image' in field_names) - self.assertTrue('image_de' in field_names) - self.assertTrue('image_en' in field_names) - - def _file_factory(self, name, content): - try: - return ContentFile(content, name=name) - except TypeError: # In Django 1.3 ContentFile had no name parameter - file = ContentFile(content) - file.name = name - return file - - def test_translated_models_instance(self): - inst = models.FileFieldsModel(title="Testtitle") - - trans_real.activate("en") - inst.title = 'title_en' - inst.file = 'a_en' - inst.file.save('b_en', ContentFile('file in english')) - inst.image = self._file_factory('i_en.jpg', 'image in english') # Direct assign - - trans_real.activate("de") - inst.title = 'title_de' - inst.file = 'a_de' - inst.file.save('b_de', ContentFile('file in german')) - inst.image = self._file_factory('i_de.jpg', 'image in german') - - inst.save() - - trans_real.activate("en") - self.assertEqual(inst.title, 'title_en') - self.assertTrue(inst.file.name.count('b_en') > 0) - self.assertEqual(inst.file.read(), b'file in english') - self.assertTrue(inst.image.name.count('i_en') > 0) - self.assertEqual(inst.image.read(), b'image in english') - - # Check if file was actually created in the global storage. - self.assertTrue(default_storage.exists(inst.file)) - self.assertTrue(inst.file.size > 0) - self.assertTrue(default_storage.exists(inst.image)) - self.assertTrue(inst.image.size > 0) - - trans_real.activate("de") - self.assertEqual(inst.title, 'title_de') - self.assertTrue(inst.file.name.count('b_de') > 0) - self.assertEqual(inst.file.read(), b'file in german') - self.assertTrue(inst.image.name.count('i_de') > 0) - self.assertEqual(inst.image.read(), b'image in german') - - inst.file_en.delete() - inst.image_en.delete() - inst.file_de.delete() - inst.image_de.delete() - - def test_empty_field(self): - from django.db.models.fields.files import FieldFile - inst = models.FileFieldsModel() - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - inst.save() - inst = models.FileFieldsModel.objects.all()[0] - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - - def test_fallback(self): - from django.db.models.fields.files import FieldFile - with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('en',)): - self.assertEqual(get_language(), 'de') - inst = models.FileFieldsModel() - inst.file_de = '' - inst.file_en = 'foo' - inst.file2_de = '' - inst.file2_en = 'bar' - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - self.assertEqual(inst.file.name, 'foo') - self.assertEqual(inst.file2.name, 'bar') - inst.save() - inst = models.FileFieldsModel.objects.all()[0] - self.assertIsInstance(inst.file, FieldFile) - self.assertIsInstance(inst.file2, FieldFile) - self.assertEqual(inst.file.name, 'foo') - self.assertEqual(inst.file2.name, 'bar') - - -class ForeignKeyFieldsTest(ModeltranslationTestBase): - @classmethod - def setUpClass(cls): - # 'model' attribute cannot be assigned to class in its definition, - # because ``models`` module will be reloaded and hence class would use old model classes. - super(ForeignKeyFieldsTest, cls).setUpClass() - cls.model = models.ForeignKeyModel - - def test_translated_models(self): - field_names = dir(self.model()) - self.assertTrue('id' in field_names) - for f in ('test', 'test_de', 'test_en', 'optional', 'optional_en', 'optional_de'): - self.assertTrue(f in field_names) - self.assertTrue('%s_id' % f in field_names) - - def test_db_column_names(self): - meta = self.model._meta - - # Make sure the correct database columns always get used: - attname, col = meta.get_field('test').get_attname_column() - self.assertEqual(attname, 'test_id') - self.assertEqual(attname, col) - - attname, col = meta.get_field('test_en').get_attname_column() - self.assertEqual(attname, 'test_en_id') - self.assertEqual(attname, col) - - attname, col = meta.get_field('test_de').get_attname_column() - self.assertEqual(attname, 'test_de_id') - self.assertEqual(attname, col) - - def test_translated_models_instance(self): - test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') - test_inst1.save() - test_inst2 = models.TestModel(title_en='title2_en', title_de='title2_de') - test_inst2.save() - inst = self.model() - - trans_real.activate("de") - inst.test = test_inst1 - inst.optional = None - - trans_real.activate("en") - # Test assigning relation by ID: - inst.optional_id = test_inst2.pk - inst.save() - - trans_real.activate("de") - self.assertEqual(inst.test_id, test_inst1.pk) - self.assertEqual(inst.test.title, 'title1_de') - self.assertEqual(inst.test_de_id, test_inst1.pk) - self.assertEqual(inst.test_de.title, 'title1_de') - self.assertEqual(inst.optional, None) - - # Test fallbacks: - trans_real.activate("en") - with default_fallback(): - self.assertEqual(inst.test_id, test_inst1.pk) - self.assertEqual(inst.test.pk, test_inst1.pk) - self.assertEqual(inst.test.title, 'title1_en') - - # Test English: - self.assertEqual(inst.optional_id, test_inst2.pk) - self.assertEqual(inst.optional.title, 'title2_en') - self.assertEqual(inst.optional_en_id, test_inst2.pk) - self.assertEqual(inst.optional_en.title, 'title2_en') - - # Test caching - inst.test_en = test_inst2 - inst.save() - trans_real.activate("de") - self.assertEqual(inst.test, test_inst1) - trans_real.activate("en") - self.assertEqual(inst.test, test_inst2) - - # Check filtering in direct way + lookup spanning - manager = self.model.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test=test_inst1).count(), 1) - self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) - self.assertEqual(manager.filter(test=test_inst2).count(), 0) - self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) - self.assertEqual(manager.filter(test__title='title1_de').count(), 1) - self.assertEqual(manager.filter(test__title='title1_en').count(), 0) - self.assertEqual(manager.filter(test__title_en='title1_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) - self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) - self.assertEqual(manager.filter(test=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) - self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) - self.assertEqual(manager.filter(test__title='title2_en').count(), 1) - self.assertEqual(manager.filter(test__title='title2_de').count(), 0) - self.assertEqual(manager.filter(test__title_de='title2_de').count(), 1) - - def test_reverse_relations(self): - test_inst = models.TestModel(title_en='title_en', title_de='title_de') - test_inst.save() - - # Instantiate many 'ForeignKeyModel' instances: - fk_inst_both = self.model(title_en='f_title_en', title_de='f_title_de', - test_de=test_inst, test_en=test_inst) - fk_inst_both.save() - fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de', - test_de_id=test_inst.pk) - fk_inst_de.save() - fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de', - test_en=test_inst) - fk_inst_en.save() - - fk_option_de = self.model.objects.create(optional_de=test_inst) - fk_option_en = self.model.objects.create(optional_en=test_inst) - - # Check that the reverse accessors are created on the model: - # Explicit related_name - testmodel_fields = models.TestModel._meta.get_all_field_names() - testmodel_methods = dir(models.TestModel) - self.assertIn('test_fks', testmodel_fields) - self.assertIn('test_fks_de', testmodel_fields) - self.assertIn('test_fks_en', testmodel_fields) - self.assertIn('test_fks', testmodel_methods) - self.assertIn('test_fks_de', testmodel_methods) - self.assertIn('test_fks_en', testmodel_methods) - # Implicit related_name: manager descriptor name != query field name - self.assertIn('foreignkeymodel', testmodel_fields) - self.assertIn('foreignkeymodel_de', testmodel_fields) - self.assertIn('foreignkeymodel_en', testmodel_fields) - self.assertIn('foreignkeymodel_set', testmodel_methods) - self.assertIn('foreignkeymodel_set_de', testmodel_methods) - self.assertIn('foreignkeymodel_set_en', testmodel_methods) - - # Check the German reverse accessor: - self.assertIn(fk_inst_both, test_inst.test_fks_de.all()) - self.assertIn(fk_inst_de, test_inst.test_fks_de.all()) - self.assertNotIn(fk_inst_en, test_inst.test_fks_de.all()) - - # Check the English reverse accessor: - self.assertIn(fk_inst_both, test_inst.test_fks_en.all()) - self.assertIn(fk_inst_en, test_inst.test_fks_en.all()) - self.assertNotIn(fk_inst_de, test_inst.test_fks_en.all()) - - # Check the default reverse accessor: - trans_real.activate("de") - self.assertIn(fk_inst_de, test_inst.test_fks.all()) - self.assertNotIn(fk_inst_en, test_inst.test_fks.all()) - trans_real.activate("en") - self.assertIn(fk_inst_en, test_inst.test_fks.all()) - self.assertNotIn(fk_inst_de, test_inst.test_fks.all()) - - # Check implicit related_name reverse accessor: - self.assertIn(fk_option_en, test_inst.foreignkeymodel_set.all()) - - # Check filtering in reverse way + lookup spanning: - manager = models.TestModel.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks__id=fk_inst_de.pk).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 0) - self.assertEqual(manager.filter(foreignkeymodel_en=fk_option_en).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 0) - self.assertEqual(manager.filter(test_fks__title_en='f_title_en').distinct().count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks__id=fk_inst_en.pk).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 1) - self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 0) - self.assertEqual(manager.filter(foreignkeymodel_de=fk_option_de).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 0) - self.assertEqual(manager.filter(test_fks__title_de='f_title_de').distinct().count(), 1) - - # Check assignment - trans_real.activate("de") - test_inst2 = models.TestModel(title_en='title_en', title_de='title_de') - test_inst2.save() - test_inst2.test_fks = [fk_inst_de, fk_inst_both] - test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) - - self.assertEqual(fk_inst_both.test.pk, test_inst2.pk) - self.assertEqual(fk_inst_both.test_id, test_inst2.pk) - self.assertEqual(fk_inst_both.test_de, test_inst2) - self.assertQuerysetsEqual(test_inst2.test_fks_de.all(), test_inst2.test_fks.all()) - self.assertIn(fk_inst_both, test_inst2.test_fks.all()) - self.assertIn(fk_inst_de, test_inst2.test_fks.all()) - self.assertNotIn(fk_inst_en, test_inst2.test_fks.all()) - trans_real.activate("en") - self.assertQuerysetsEqual(test_inst2.test_fks_en.all(), test_inst2.test_fks.all()) - self.assertIn(fk_inst_both, test_inst2.test_fks.all()) - self.assertIn(fk_inst_en, test_inst2.test_fks.all()) - self.assertNotIn(fk_inst_de, test_inst2.test_fks.all()) - - def test_non_translated_relation(self): - non_de = models.NonTranslated.objects.create(title='title_de') - non_en = models.NonTranslated.objects.create(title='title_en') - - fk_inst_both = self.model.objects.create( - title_en='f_title_en', title_de='f_title_de', non_de=non_de, non_en=non_en) - fk_inst_de = self.model.objects.create(non_de=non_de) - fk_inst_en = self.model.objects.create(non_en=non_en) - - # Forward relation + spanning - manager = self.model.objects - trans_real.activate("de") - self.assertEqual(manager.filter(non=non_de).count(), 2) - self.assertEqual(manager.filter(non=non_en).count(), 0) - self.assertEqual(manager.filter(non_en=non_en).count(), 2) - self.assertEqual(manager.filter(non__title='title_de').count(), 2) - self.assertEqual(manager.filter(non__title='title_en').count(), 0) - self.assertEqual(manager.filter(non_en__title='title_en').count(), 2) - trans_real.activate("en") - self.assertEqual(manager.filter(non=non_en).count(), 2) - self.assertEqual(manager.filter(non=non_de).count(), 0) - self.assertEqual(manager.filter(non_de=non_de).count(), 2) - self.assertEqual(manager.filter(non__title='title_en').count(), 2) - self.assertEqual(manager.filter(non__title='title_de').count(), 0) - self.assertEqual(manager.filter(non_de__title='title_de').count(), 2) - - # Reverse relation + spanning - manager = models.NonTranslated.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 0) - self.assertEqual(manager.filter(test_fks__title_en='f_title_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 1) - self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 0) - self.assertEqual(manager.filter(test_fks__title_de='f_title_de').count(), 1) - - def assertQuerysetsEqual(self, qs1, qs2): - pk = lambda o: o.pk - return self.assertEqual(sorted(qs1, key=pk), sorted(qs2, key=pk)) - - -class OneToOneFieldsTest(ForeignKeyFieldsTest): - @classmethod - def setUpClass(cls): - # 'model' attribute cannot be assigned to class in its definition, - # because ``models`` module will be reloaded and hence class would use old model classes. - super(OneToOneFieldsTest, cls).setUpClass() - cls.model = models.OneToOneFieldModel - - def test_uniqueness(self): - test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') - test_inst1.save() - inst = self.model() - - trans_real.activate("de") - inst.test = test_inst1 - - trans_real.activate("en") - # That's ok, since test_en is different than test_de - inst.test = test_inst1 - inst.save() - - # But this violates uniqueness constraint - inst2 = self.model(test=test_inst1) - self.assertRaises(IntegrityError, inst2.save) - - def test_reverse_relations(self): - test_inst = models.TestModel(title_en='title_en', title_de='title_de') - test_inst.save() - - # Instantiate many 'OneToOneFieldModel' instances: - fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de', - test_de_id=test_inst.pk) - fk_inst_de.save() - fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de', - test_en=test_inst) - fk_inst_en.save() - - fk_option_de = self.model.objects.create(optional_de=test_inst) - fk_option_en = self.model.objects.create(optional_en=test_inst) - - # Check that the reverse accessors are created on the model: - # Explicit related_name - testmodel_fields = models.TestModel._meta.get_all_field_names() - testmodel_methods = dir(models.TestModel) - self.assertIn('test_o2o', testmodel_fields) - self.assertIn('test_o2o_de', testmodel_fields) - self.assertIn('test_o2o_en', testmodel_fields) - self.assertIn('test_o2o', testmodel_methods) - self.assertIn('test_o2o_de', testmodel_methods) - self.assertIn('test_o2o_en', testmodel_methods) - # Implicit related_name - self.assertIn('onetoonefieldmodel', testmodel_fields) - self.assertIn('onetoonefieldmodel_de', testmodel_fields) - self.assertIn('onetoonefieldmodel_en', testmodel_fields) - self.assertIn('onetoonefieldmodel', testmodel_methods) - self.assertIn('onetoonefieldmodel_de', testmodel_methods) - self.assertIn('onetoonefieldmodel_en', testmodel_methods) - - # Check the German reverse accessor: - self.assertEqual(fk_inst_de, test_inst.test_o2o_de) - - # Check the English reverse accessor: - self.assertEqual(fk_inst_en, test_inst.test_o2o_en) - - # Check the default reverse accessor: - trans_real.activate("de") - self.assertEqual(fk_inst_de, test_inst.test_o2o) - trans_real.activate("en") - self.assertEqual(fk_inst_en, test_inst.test_o2o) - - # Check implicit related_name reverse accessor: - self.assertEqual(fk_option_en, test_inst.onetoonefieldmodel) - - # Check filtering in reverse way + lookup spanning: - manager = models.TestModel.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_o2o__id=fk_inst_de.pk).count(), 1) - self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 1) - self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 0) - self.assertEqual(manager.filter(onetoonefieldmodel_en=fk_option_en).count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 0) - self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').distinct().count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_o2o__id=fk_inst_en.pk).count(), 1) - self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 1) - self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 0) - self.assertEqual(manager.filter(onetoonefieldmodel_de=fk_option_de).count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 0) - self.assertEqual(manager.filter(test_o2o__title_de='f_title_de').distinct().count(), 1) - - # Check assignment - trans_real.activate("de") - test_inst2 = models.TestModel(title_en='title_en', title_de='title_de') - test_inst2.save() - test_inst2.test_o2o = fk_inst_de - test_inst2.test_o2o_en = fk_inst_en - - self.assertEqual(fk_inst_de.test.pk, test_inst2.pk) - self.assertEqual(fk_inst_de.test_id, test_inst2.pk) - self.assertEqual(fk_inst_de.test_de, test_inst2) - self.assertEqual(test_inst2.test_o2o_de, test_inst2.test_o2o) - self.assertEqual(fk_inst_de, test_inst2.test_o2o) - trans_real.activate("en") - self.assertEqual(fk_inst_en.test.pk, test_inst2.pk) - self.assertEqual(fk_inst_en.test_id, test_inst2.pk) - self.assertEqual(fk_inst_en.test_en, test_inst2) - self.assertEqual(test_inst2.test_o2o_en, test_inst2.test_o2o) - self.assertEqual(fk_inst_en, test_inst2.test_o2o) - - def test_non_translated_relation(self): - non_de = models.NonTranslated.objects.create(title='title_de') - non_en = models.NonTranslated.objects.create(title='title_en') - - fk_inst_de = self.model.objects.create( - title_en='f_title_en', title_de='f_title_de', non_de=non_de) - fk_inst_en = self.model.objects.create( - title_en='f_title_en2', title_de='f_title_de2', non_en=non_en) - - # Forward relation + spanning - manager = self.model.objects - trans_real.activate("de") - self.assertEqual(manager.filter(non=non_de).count(), 1) - self.assertEqual(manager.filter(non=non_en).count(), 0) - self.assertEqual(manager.filter(non_en=non_en).count(), 1) - self.assertEqual(manager.filter(non__title='title_de').count(), 1) - self.assertEqual(manager.filter(non__title='title_en').count(), 0) - self.assertEqual(manager.filter(non_en__title='title_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(non=non_en).count(), 1) - self.assertEqual(manager.filter(non=non_de).count(), 0) - self.assertEqual(manager.filter(non_de=non_de).count(), 1) - self.assertEqual(manager.filter(non__title='title_en').count(), 1) - self.assertEqual(manager.filter(non__title='title_de').count(), 0) - self.assertEqual(manager.filter(non_de__title='title_de').count(), 1) - - # Reverse relation + spanning - manager = models.NonTranslated.objects - trans_real.activate("de") - self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0) - self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_de').count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_en').count(), 0) - self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').count(), 1) - trans_real.activate("en") - self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1) - self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0) - self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_en2').count(), 1) - self.assertEqual(manager.filter(test_o2o__title='f_title_de2').count(), 0) - self.assertEqual(manager.filter(test_o2o__title_de='f_title_de2').count(), 1) - - -class OtherFieldsTest(ModeltranslationTestBase): - def test_translated_models(self): - inst = models.OtherFieldsModel.objects.create() - field_names = dir(inst) - self.assertTrue('id' in field_names) - self.assertTrue('int' in field_names) - self.assertTrue('int_de' in field_names) - self.assertTrue('int_en' in field_names) - self.assertTrue('boolean' in field_names) - self.assertTrue('boolean_de' in field_names) - self.assertTrue('boolean_en' in field_names) - self.assertTrue('nullboolean' in field_names) - self.assertTrue('nullboolean_de' in field_names) - self.assertTrue('nullboolean_en' in field_names) - self.assertTrue('csi' in field_names) - self.assertTrue('csi_de' in field_names) - self.assertTrue('csi_en' in field_names) - self.assertTrue('ip' in field_names) - self.assertTrue('ip_de' in field_names) - self.assertTrue('ip_en' in field_names) -# self.assertTrue('genericip' in field_names) -# self.assertTrue('genericip_de' in field_names) -# self.assertTrue('genericip_en' in field_names) - self.assertTrue('float' in field_names) - self.assertTrue('float_de' in field_names) - self.assertTrue('float_en' in field_names) - self.assertTrue('decimal' in field_names) - self.assertTrue('decimal_de' in field_names) - self.assertTrue('decimal_en' in field_names) - inst.delete() - - def test_translated_models_integer_instance(self): - inst = models.OtherFieldsModel() - inst.int = 7 - self.assertEqual('de', get_language()) - self.assertEqual(7, inst.int) - self.assertEqual(7, inst.int_de) - self.assertEqual(42, inst.int_en) # default value is honored - - inst.int += 2 - inst.save() - self.assertEqual(9, inst.int) - self.assertEqual(9, inst.int_de) - self.assertEqual(42, inst.int_en) - - trans_real.activate('en') - inst.int -= 1 - self.assertEqual(41, inst.int) - self.assertEqual(9, inst.int_de) - self.assertEqual(41, inst.int_en) - - # this field has validator - let's try to make it below 0! - inst.int -= 50 - self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_boolean_instance(self): - inst = models.OtherFieldsModel() - inst.boolean = True - self.assertEqual('de', get_language()) - self.assertEqual(True, inst.boolean) - self.assertEqual(True, inst.boolean_de) - self.assertEqual(False, inst.boolean_en) - - inst.boolean = False - inst.save() - self.assertEqual(False, inst.boolean) - self.assertEqual(False, inst.boolean_de) - self.assertEqual(False, inst.boolean_en) - - trans_real.activate('en') - inst.boolean = True - self.assertEqual(True, inst.boolean) - self.assertEqual(False, inst.boolean_de) - self.assertEqual(True, inst.boolean_en) - - def test_translated_models_nullboolean_instance(self): - inst = models.OtherFieldsModel() - inst.nullboolean = True - self.assertEqual('de', get_language()) - self.assertEqual(True, inst.nullboolean) - self.assertEqual(True, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - inst.nullboolean = False - inst.save() - self.assertEqual(False, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - trans_real.activate('en') - inst.nullboolean = True - self.assertEqual(True, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(True, inst.nullboolean_en) - - inst.nullboolean = None - self.assertEqual(None, inst.nullboolean) - self.assertEqual(False, inst.nullboolean_de) - self.assertEqual(None, inst.nullboolean_en) - - def test_translated_models_commaseparatedinteger_instance(self): - inst = models.OtherFieldsModel() - inst.csi = '4,8,15,16,23,42' - self.assertEqual('de', get_language()) - self.assertEqual('4,8,15,16,23,42', inst.csi) - self.assertEqual('4,8,15,16,23,42', inst.csi_de) - self.assertEqual(None, inst.csi_en) - - inst.csi = '23,42' - inst.save() - self.assertEqual('23,42', inst.csi) - self.assertEqual('23,42', inst.csi_de) - self.assertEqual(None, inst.csi_en) - - trans_real.activate('en') - inst.csi = '4,8,15,16,23,42' - self.assertEqual('4,8,15,16,23,42', inst.csi) - self.assertEqual('23,42', inst.csi_de) - self.assertEqual('4,8,15,16,23,42', inst.csi_en) - - # Now that we have covered csi, lost, illuminati and hitchhiker - # compliance in a single test, do something useful... - - # Check if validation is preserved - inst.csi = '1;2' - self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_ipaddress_instance(self): - inst = models.OtherFieldsModel() - inst.ip = '192.0.1.42' - self.assertEqual('de', get_language()) - self.assertEqual('192.0.1.42', inst.ip) - self.assertEqual('192.0.1.42', inst.ip_de) - self.assertEqual(None, inst.ip_en) - - inst.ip = '192.0.23.1' - inst.save() - self.assertEqual('192.0.23.1', inst.ip) - self.assertEqual('192.0.23.1', inst.ip_de) - self.assertEqual(None, inst.ip_en) - - trans_real.activate('en') - inst.ip = '192.0.1.42' - self.assertEqual('192.0.1.42', inst.ip) - self.assertEqual('192.0.23.1', inst.ip_de) - self.assertEqual('192.0.1.42', inst.ip_en) - - # Check if validation is preserved - inst.ip = '1;2' - self.assertRaises(ValidationError, inst.full_clean) - -# def test_translated_models_genericipaddress_instance(self): -# inst = OtherFieldsModel() -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('de', get_language()) -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:42fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# inst.genericip = '2a02:23fe::4' -# inst.save() -# self.assertEqual('2a02:23fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual(None, inst.genericip_en) -# -# trans_real.activate('en') -# inst.genericip = '2a02:42fe::4' -# self.assertEqual('2a02:42fe::4', inst.genericip) -# self.assertEqual('2a02:23fe::4', inst.genericip_de) -# self.assertEqual('2a02:42fe::4', inst.genericip_en) -# -# # Check if validation is preserved -# inst.genericip = '1;2' -# self.assertRaises(ValidationError, inst.full_clean) - - def test_translated_models_float_instance(self): - inst = models.OtherFieldsModel() - inst.float = 0.42 - self.assertEqual('de', get_language()) - self.assertEqual(0.42, inst.float) - self.assertEqual(0.42, inst.float_de) - self.assertEqual(None, inst.float_en) - - inst.float = 0.23 - inst.save() - self.assertEqual(0.23, inst.float) - self.assertEqual(0.23, inst.float_de) - self.assertEqual(None, inst.float_en) - - inst.float += 0.08 - self.assertEqual(0.31, inst.float) - self.assertEqual(0.31, inst.float_de) - self.assertEqual(None, inst.float_en) - - trans_real.activate('en') - inst.float = 0.42 - self.assertEqual(0.42, inst.float) - self.assertEqual(0.31, inst.float_de) - self.assertEqual(0.42, inst.float_en) - - def test_translated_models_decimal_instance(self): - inst = models.OtherFieldsModel() - inst.decimal = Decimal('0.42') - self.assertEqual('de', get_language()) - self.assertEqual(Decimal('0.42'), inst.decimal) - self.assertEqual(Decimal('0.42'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - inst.decimal = inst.decimal - Decimal('0.19') - inst.save() - self.assertEqual(Decimal('0.23'), inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - trans_real.activate('en') - self.assertRaises(TypeError, lambda x: inst.decimal + Decimal('0.19')) - self.assertEqual(None, inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(None, inst.decimal_en) - - inst.decimal = Decimal('0.42') - self.assertEqual(Decimal('0.42'), inst.decimal) - self.assertEqual(Decimal('0.23'), inst.decimal_de) - self.assertEqual(Decimal('0.42'), inst.decimal_en) - - def test_translated_models_date_instance(self): - inst = models.OtherFieldsModel() - inst.date = datetime.date(2012, 12, 31) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.date(2012, 12, 31), inst.date) - self.assertEqual(datetime.date(2012, 12, 31), inst.date_de) - self.assertEqual(None, inst.date_en) - - inst.date = datetime.date(1999, 1, 1) - inst.save() - self.assertEqual(datetime.date(1999, 1, 1), inst.date) - self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) - self.assertEqual(None, inst.date_en) - - qs = models.OtherFieldsModel.objects.filter(date='1999-1-1') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].date, datetime.date(1999, 1, 1)) - - trans_real.activate('en') - inst.date = datetime.date(2012, 12, 31) - self.assertEqual(datetime.date(2012, 12, 31), inst.date) - self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) - self.assertEqual(datetime.date(2012, 12, 31), inst.date_en) - - def test_translated_models_datetime_instance(self): - inst = models.OtherFieldsModel() - inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_de) - self.assertEqual(None, inst.datetime_en) - - inst.datetime = datetime.datetime(1999, 1, 1, 23, 42) - inst.save() - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) - self.assertEqual(None, inst.datetime_en) - - qs = models.OtherFieldsModel.objects.filter(datetime='1999-1-1 23:42') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].datetime, datetime.datetime(1999, 1, 1, 23, 42)) - - trans_real.activate('en') - inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) - self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) - self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_en) - - def test_translated_models_time_instance(self): - inst = models.OtherFieldsModel() - inst.time = datetime.time(23, 42, 0) - self.assertEqual('de', get_language()) - self.assertEqual(datetime.time(23, 42, 0), inst.time) - self.assertEqual(datetime.time(23, 42, 0), inst.time_de) - self.assertEqual(None, inst.time_en) - - inst.time = datetime.time(1, 2, 3) - inst.save() - self.assertEqual(datetime.time(1, 2, 3), inst.time) - self.assertEqual(datetime.time(1, 2, 3), inst.time_de) - self.assertEqual(None, inst.time_en) - - qs = models.OtherFieldsModel.objects.filter(time='01:02:03') - self.assertEqual(len(qs), 1) - self.assertEqual(qs[0].time, datetime.time(1, 2, 3)) - - trans_real.activate('en') - inst.time = datetime.time(23, 42, 0) - self.assertEqual(datetime.time(23, 42, 0), inst.time) - self.assertEqual(datetime.time(1, 2, 3), inst.time_de) - self.assertEqual(datetime.time(23, 42, 0), inst.time_en) - - def test_descriptors(self): - # Descriptor store ints in database and returns string of 'a' of that length - inst = models.DescriptorModel() - # Demonstrate desired behaviour - inst.normal = 2 - self.assertEqual('aa', inst.normal) - inst.normal = 'abc' - self.assertEqual('aaa', inst.normal) - - # Descriptor on translated field works too - self.assertEqual('de', get_language()) - inst.trans = 5 - self.assertEqual('aaaaa', inst.trans) - - inst.save() - db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] - self.assertEqual(3, db_values['normal']) - self.assertEqual(5, db_values['trans_de']) - self.assertEqual(0, db_values['trans_en']) - - # Retrieval from db - inst = models.DescriptorModel.objects.all()[0] - self.assertEqual('aaa', inst.normal) - self.assertEqual('aaaaa', inst.trans) - self.assertEqual('aaaaa', inst.trans_de) - self.assertEqual('', inst.trans_en) - - # Other language - trans_real.activate('en') - self.assertEqual('', inst.trans) - inst.trans = 'q' - self.assertEqual('a', inst.trans) - inst.trans_de = 4 - self.assertEqual('aaaa', inst.trans_de) - inst.save() - db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] - self.assertEqual(3, db_values['normal']) - self.assertEqual(4, db_values['trans_de']) - self.assertEqual(1, db_values['trans_en']) - - -class ModeltranslationTestRule1(ModeltranslationTestBase): - """ - Rule 1: Reading the value from the original field returns the value in - translated to the current language. - """ - def _test_field(self, field_name, value_de, value_en, deactivate=True): - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - params = {field_name_de: value_de, field_name_en: value_en} - - n = models.TestModel.objects.create(**params) - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - self.assertEqual(getattr(n, field_name), value_de) - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - # Now switch to "en" - trans_real.activate("en") - self.assertEqual(get_language(), "en") - # Should now be return the english one (just by switching the language) - self.assertEqual(getattr(n, field_name), value_en) - # But explicit language fields hold their values - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - - n = models.TestModel.objects.create(**params) - n.save() - # Language is set to "en" at this point - self.assertEqual(get_language(), "en") - self.assertEqual(getattr(n, field_name), value_en) - self.assertEqual(getattr(n, field_name_de), value_de) - self.assertEqual(getattr(n, field_name_en), value_en) - trans_real.activate('de') - self.assertEqual(get_language(), 'de') - self.assertEqual(getattr(n, field_name), value_de) - - if deactivate: - trans_real.deactivate() - - def test_rule1(self): - """ - Basic CharField/TextField test. - """ - title1_de = "title de" - title1_en = "title en" - text_de = "Dies ist ein deutscher Satz" - text_en = "This is an english sentence" - - self._test_field(field_name='title', value_de=title1_de, value_en=title1_en) - self._test_field(field_name='text', value_de=text_de, value_en=text_en) - - def test_rule1_url_field(self): - self._test_field(field_name='url', - value_de='http://www.google.de', - value_en='http://www.google.com') - - def test_rule1_email_field(self): - self._test_field(field_name='email', - value_de='django-modeltranslation@googlecode.de', - value_en='django-modeltranslation@googlecode.com') - - -class ModeltranslationTestRule2(ModeltranslationTestBase): - """ - Rule 2: Assigning a value to the original field updates the value - in the associated current language translation field. - """ - def _test_field(self, field_name, value1_de, value1_en, value2, value3, - deactivate=True): - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - params = {field_name_de: value1_de, field_name_en: value1_en} - - self.assertEqual(get_language(), 'de') - n = models.TestModel.objects.create(**params) - self.assertEqual(getattr(n, field_name), value1_de) - self.assertEqual(getattr(n, field_name_de), value1_de) - self.assertEqual(getattr(n, field_name_en), value1_en) - - setattr(n, field_name, value2) - n.save() - self.assertEqual(getattr(n, field_name), value2) - self.assertEqual(getattr(n, field_name_de), value2) - self.assertEqual(getattr(n, field_name_en), value1_en) - - trans_real.activate("en") - self.assertEqual(get_language(), "en") - - setattr(n, field_name, value3) - setattr(n, field_name_de, value1_de) - n.save() - self.assertEqual(getattr(n, field_name), value3) - self.assertEqual(getattr(n, field_name_en), value3) - self.assertEqual(getattr(n, field_name_de), value1_de) - - if deactivate: - trans_real.deactivate() - - def test_rule2(self): - """ - Basic CharField/TextField test. - """ - self._test_field(field_name='title', - value1_de='title de', - value1_en='title en', - value2='Neuer Titel', - value3='new title') - - def test_rule2_url_field(self): - self._test_field(field_name='url', - value1_de='http://www.google.de', - value1_en='http://www.google.com', - value2='http://www.google.at', - value3='http://www.google.co.uk') - - def test_rule2_email_field(self): - self._test_field(field_name='email', - value1_de='django-modeltranslation@googlecode.de', - value1_en='django-modeltranslation@googlecode.com', - value2='django-modeltranslation@googlecode.at', - value3='django-modeltranslation@googlecode.co.uk') - - -class ModeltranslationTestRule3(ModeltranslationTestBase): - """ - Rule 3: If both fields - the original and the current language translation - field - are updated at the same time, the current language translation - field wins. - """ - - def test_rule3(self): - self.assertEqual(get_language(), 'de') - title = 'title de' - - # Normal behaviour - n = models.TestModel(title='foo') - self.assertEqual(n.title, 'foo') - self.assertEqual(n.title_de, 'foo') - self.assertEqual(n.title_en, None) - - # constructor - n = models.TestModel(title_de=title, title='foo') - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # object.create - n = models.TestModel.objects.create(title_de=title, title='foo') - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # Database save/load - n = models.TestModel.objects.get(title_de=title) - self.assertEqual(n.title, title) - self.assertEqual(n.title_de, title) - self.assertEqual(n.title_en, None) - - # This is not subject to Rule 3, because updates are not *at the ame time* - n = models.TestModel() - n.title_de = title - n.title = 'foo' - self.assertEqual(n.title, 'foo') - self.assertEqual(n.title_de, 'foo') - self.assertEqual(n.title_en, None) - - @staticmethod - def _index(list, element): - for i, el in enumerate(list): - if el is element: - return i - raise ValueError - - def test_rule3_internals(self): - # Rule 3 work because translation fields are added to model field list - # later than original field. - original = models.TestModel._meta.get_field('title') - translated_de = models.TestModel._meta.get_field('title_de') - translated_en = models.TestModel._meta.get_field('title_en') - fields = models.TestModel._meta.fields - # Here we cannot use simple list.index, because Field has overloaded __cmp__ - self.assertTrue(self._index(fields, original) < self._index(fields, translated_de)) - self.assertTrue(self._index(fields, original) < self._index(fields, translated_en)) - - -class ModelValidationTest(ModeltranslationTestBase): - """ - Tests if a translation model field validates correctly. - """ - def assertRaisesValidation(self, func): - try: - func() - except ValidationError as e: - return e.message_dict - self.fail('ValidationError not raised.') - - def _test_model_validation(self, field_name, invalid_value, valid_value): - """ - Generic model field validation test. - """ - field_name_de = '%s_de' % field_name - field_name_en = '%s_en' % field_name - # Title need to be passed here - otherwise it would not validate - params = {'title_de': 'title de', 'title_en': 'title en', field_name: invalid_value} - - n = models.TestModel.objects.create(**params) - - # First check the original field - # Expect that the validation object contains an error - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - - # Set translation field to a valid value - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - setattr(n, field_name_de, valid_value) - n.full_clean() - - # All language fields are validated even though original field validation raise no error - setattr(n, field_name_en, invalid_value) - errors = self.assertRaisesValidation(n.full_clean) - self.assertNotIn(field_name, errors) - self.assertIn(field_name_en, errors) - - # When language is changed to en, the original field also doesn't validate - with override('en'): - setattr(n, field_name_en, invalid_value) - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - self.assertIn(field_name_en, errors) - - # Set translation field to an invalid value - setattr(n, field_name_en, valid_value) - setattr(n, field_name_de, invalid_value) - # Expect that the validation object contains an error for url_de - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn(field_name, errors) - self.assertIn(field_name_de, errors) - - def test_model_validation_required(self): - """ - General test for CharField: if required/blank is handled properly. - """ - # Create an object without title (which is required) - n = models.TestModel.objects.create(text='Testtext') - - # First check the original field - # Expect that the validation object contains an error for title - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn('title', errors) - n.save() - - # Check the translation field - # Language is set to 'de' at this point - self.assertEqual(get_language(), 'de') - # Set translation field to a valid title - n.title_de = 'Title' - n.full_clean() - - # Change language to en - # Now validation fails, because current language (en) title is empty - # So requirement validation depends on current language - with override('en'): - errors = self.assertRaisesValidation(n.full_clean) - self.assertIn('title', errors) - - # However, with fallback language (most cases), it validates (because empty title - # falls back to title_de): - with default_fallback(): - n.full_clean() - - # Set translation field to an empty title - n.title_de = None - # Even though the original field isn't optional, translation fields are - # per definition always optional. So we expect that the validation - # object contains no error for title_de. - # However, title still raises error, since it points to empty title_de - errors = self.assertRaisesValidation(n.full_clean) - self.assertNotIn('title_de', errors) - self.assertIn('title', errors) - - def test_model_validation_url_field(self): - self._test_model_validation( - field_name='url', - invalid_value='foo en', - valid_value='http://code.google.com/p/django-modeltranslation/') - - def test_model_validation_email_field(self): - self._test_model_validation( - field_name='email', invalid_value='foo en', - valid_value='django-modeltranslation@googlecode.com') - - -class ModelInheritanceTest(ModeltranslationTestBase): - """Tests for inheritance support in modeltranslation.""" - def test_abstract_inheritance(self): - field_names_b = models.AbstractModelB._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_b) - self.assertTrue('titlea_de' in field_names_b) - self.assertTrue('titlea_en' in field_names_b) - self.assertTrue('titleb' in field_names_b) - self.assertTrue('titleb_de' in field_names_b) - self.assertTrue('titleb_en' in field_names_b) - self.assertFalse('titled' in field_names_b) - self.assertFalse('titled_de' in field_names_b) - self.assertFalse('titled_en' in field_names_b) - - def test_multitable_inheritance(self): - field_names_a = models.MultitableModelA._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_a) - self.assertTrue('titlea_de' in field_names_a) - self.assertTrue('titlea_en' in field_names_a) - - field_names_b = models.MultitableModelB._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_b) - self.assertTrue('titlea_de' in field_names_b) - self.assertTrue('titlea_en' in field_names_b) - self.assertTrue('titleb' in field_names_b) - self.assertTrue('titleb_de' in field_names_b) - self.assertTrue('titleb_en' in field_names_b) - - field_names_c = models.MultitableModelC._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_c) - self.assertTrue('titlea_de' in field_names_c) - self.assertTrue('titlea_en' in field_names_c) - self.assertTrue('titleb' in field_names_c) - self.assertTrue('titleb_de' in field_names_c) - self.assertTrue('titleb_en' in field_names_c) - self.assertTrue('titlec' in field_names_c) - self.assertTrue('titlec_de' in field_names_c) - self.assertTrue('titlec_en' in field_names_c) - - field_names_d = models.MultitableModelD._meta.get_all_field_names() - self.assertTrue('titlea' in field_names_d) - self.assertTrue('titlea_de' in field_names_d) - self.assertTrue('titlea_en' in field_names_d) - self.assertTrue('titleb' in field_names_d) - self.assertTrue('titleb_de' in field_names_d) - self.assertTrue('titleb_en' in field_names_d) - self.assertTrue('titled' in field_names_d) - - def test_inheritance(self): - def assertLocalFields(model, local_fields): - # Proper fields are inherited. - opts = translator.translator.get_options_for_model(model) - self.assertEqual(set(opts.local_fields.keys()), set(local_fields)) - # Local translation fields are created on the model. - model_local_fields = [f.name for f in model._meta.local_fields] - for field in local_fields: - for lang in mt_settings.AVAILABLE_LANGUAGES: - translation_field = build_localized_fieldname(field, lang) - self.assertTrue(translation_field in model_local_fields) - - def assertFields(model, fields): - # The given fields are inherited. - opts = translator.translator.get_options_for_model(model) - self.assertEqual(set(opts.fields.keys()), set(fields)) - # Inherited translation fields are available on the model. - model_fields = model._meta.get_all_field_names() - for field in fields: - for lang in mt_settings.AVAILABLE_LANGUAGES: - translation_field = build_localized_fieldname(field, lang) - self.assertTrue(translation_field in model_fields) - - # Translation fields can be declared on abstract classes. - assertLocalFields(models.Slugged, ('slug',)) - assertLocalFields(models.MetaData, ('keywords',)) - assertLocalFields(models.RichText, ('content',)) - # Local fields are inherited from abstract superclasses. - assertLocalFields(models.Displayable, ('slug', 'keywords',)) - assertLocalFields(models.Page, ('slug', 'keywords', 'title',)) - # But not from concrete superclasses. - assertLocalFields(models.RichTextPage, ('content',)) - - # Fields inherited from concrete models are also available. - assertFields(models.Slugged, ('slug',)) - assertFields(models.Page, ('slug', 'keywords', 'title',)) - assertFields(models.RichTextPage, ('slug', 'keywords', 'title', - 'content',)) - - -class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase): - """ - Tests for inheritance support with field aggregation - in modeltranslation. - """ - def test_field_aggregation(self): - clsb = FieldInheritanceCTranslationOptions - self.assertTrue('titlea' in clsb.fields) - self.assertTrue('titleb' in clsb.fields) - self.assertTrue('titlec' in clsb.fields) - self.assertEqual(3, len(clsb.fields)) - self.assertEqual(tuple, type(clsb.fields)) - - def test_multi_inheritance(self): - clsb = FieldInheritanceETranslationOptions - self.assertTrue('titlea' in clsb.fields) - self.assertTrue('titleb' in clsb.fields) - self.assertTrue('titlec' in clsb.fields) - self.assertTrue('titled' in clsb.fields) - self.assertTrue('titlee' in clsb.fields) - self.assertEqual(5, len(clsb.fields)) # there are no repetitions - - -class UpdateCommandTest(ModeltranslationTestBase): - def test_update_command(self): - # Here it would be convenient to use fixtures - unfortunately, - # fixtures loader doesn't use raw sql but rather creates objects, - # so translation descriptor affects result and we cannot set the - # 'original' field value. - pk1 = models.TestModel.objects.create(title_de='').pk - pk2 = models.TestModel.objects.create(title_de='already').pk - # Due to ``rewrite(False)`` here, original field will be affected. - models.TestModel.objects.all().rewrite(False).update(title='initial') - - # Check raw data using ``values`` - obj1 = models.TestModel.objects.filter(pk=pk1).raw_values()[0] - obj2 = models.TestModel.objects.filter(pk=pk2).raw_values()[0] - self.assertEqual('', obj1['title_de']) - self.assertEqual('initial', obj1['title']) - self.assertEqual('already', obj2['title_de']) - self.assertEqual('initial', obj2['title']) - - call_command('update_translation_fields', verbosity=0) - - obj1 = models.TestModel.objects.get(pk=pk1) - obj2 = models.TestModel.objects.get(pk=pk2) - self.assertEqual('initial', obj1.title_de) - self.assertEqual('already', obj2.title_de) - - -class TranslationAdminTest(ModeltranslationTestBase): - def setUp(self): - super(TranslationAdminTest, self).setUp() - self.test_obj = models.TestModel.objects.create( - title='Testtitle', text='Testtext') - self.site = AdminSite() - - def tearDown(self): - self.test_obj.delete() - super(TranslationAdminTest, self).tearDown() - - def test_default_fields(self): - class TestModelAdmin(admin.TranslationAdmin): - pass - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('title_de', 'title_en', 'text_de', 'text_en', 'url_de', 'url_en', - 'email_de', 'email_en')) - - def test_default_fieldsets(self): - class TestModelAdmin(admin.TranslationAdmin): - pass - - ma = TestModelAdmin(models.TestModel, self.site) - # We expect that the original field is excluded and only the - # translation fields are included in fields - fields = ['title_de', 'title_en', 'text_de', 'text_en', - 'url_de', 'url_en', 'email_de', 'email_en'] - self.assertEqual( - ma.get_fieldsets(request), [(None, {'fields': fields})]) - self.assertEqual( - ma.get_fieldsets(request, self.test_obj), - [(None, {'fields': fields})]) - - def test_field_arguments(self): - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_field_arguments_restricted_on_form(self): - # Using `fields`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `fieldsets`. - class TestModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ['title']})] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - exclude = ['url', 'email'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - - # You can also pass a tuple to `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - exclude = ('url', 'email') - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `fields` and `exclude`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title', 'url'] - exclude = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) - - # Using `fields` and `readonly_fields`. - class TestModelAdmin(admin.TranslationAdmin): - fields = ['title', 'url'] - readonly_fields = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) - - # Using `readonly_fields`. - # Note: readonly fields are not included in the form. - class TestModelAdmin(admin.TranslationAdmin): - readonly_fields = ['title'] - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('text_de', 'text_en', 'url_de', 'url_en', 'email_de', 'email_en')) - - # Using grouped fields. - # Note: Current implementation flattens the nested fields. - class TestModelAdmin(admin.TranslationAdmin): - fields = (('title', 'url'), 'email',) - - ma = TestModelAdmin(models.TestModel, self.site) - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), - ('title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en')) - - # Using grouped fields in `fieldsets`. - class TestModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ('email', ('title', 'url'))})] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['email_de', 'email_en', 'title_de', 'title_en', 'url_de', 'url_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_field_arguments_restricted_on_custom_form(self): - # Using `fields`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - fields = ['url', 'email'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['url_de', 'url_en', 'email_de', 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Using `exclude`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - exclude = ['url', 'email'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # If both, the custom form an the ModelAdmin define an `exclude` - # option, the ModelAdmin wins. This is Django behaviour. - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - exclude = ['url'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['title_de', 'title_en', 'text_de', 'text_en', 'email_de', - 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Same for `fields`. - class TestModelForm(forms.ModelForm): - class Meta: - model = models.TestModel - fields = ['text', 'title'] - - class TestModelAdmin(admin.TranslationAdmin): - form = TestModelForm - fields = ['email'] - - ma = TestModelAdmin(models.TestModel, self.site) - fields = ['email_de', 'email_en'] - self.assertEqual( - tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - def test_inline_fieldsets(self): - class DataInline(admin.TranslationStackedInline): - model = models.DataModel - fieldsets = [ - ('Test', {'fields': ('data',)}) - ] - - class TestModelAdmin(admin.TranslationAdmin): - exclude = ('title', 'text',) - inlines = [DataInline] - - class DataTranslationOptions(translator.TranslationOptions): - fields = ('data',) - - translator.translator.register(models.DataModel, - DataTranslationOptions) - ma = TestModelAdmin(models.TestModel, self.site) - - fieldsets = [('Test', {'fields': ['data_de', 'data_en']})] - - try: - ma_fieldsets = ma.get_inline_instances( - request)[0].get_fieldsets(request) - except AttributeError: # Django 1.3 fallback - ma_fieldsets = ma.inlines[0]( - models.TestModel, self.site).get_fieldsets(request) - self.assertEqual(ma_fieldsets, fieldsets) - - try: - ma_fieldsets = ma.get_inline_instances( - request)[0].get_fieldsets(request, self.test_obj) - except AttributeError: # Django 1.3 fallback - ma_fieldsets = ma.inlines[0]( - models.TestModel, self.site).get_fieldsets(request, self.test_obj) - self.assertEqual(ma_fieldsets, fieldsets) - - # Remove translation for DataModel - translator.translator.unregister(models.DataModel) - - def test_build_css_class(self): - with reload_override_settings(LANGUAGES=(('de', 'German'), ('en', 'English'), - ('es-ar', 'Argentinian Spanish'),)): - fields = { - 'foo_en': 'foo-en', - 'foo_es_ar': 'foo-es_ar', - 'foo_en_us': 'foo-en_us', - 'foo_bar_de': 'foo_bar-de', - '_foo_en': '_foo-en', - '_foo_es_ar': '_foo-es_ar', - '_foo_bar_de': '_foo_bar-de', - 'foo__en': 'foo_-en', - 'foo__es_ar': 'foo_-es_ar', - 'foo_bar__de': 'foo_bar_-de', - } - for field, css in fields.items(): - self.assertEqual(build_css_class(field), css) - - def test_multitable_inheritance(self): - class MultitableModelAAdmin(admin.TranslationAdmin): - pass - - class MultitableModelBAdmin(admin.TranslationAdmin): - pass - - maa = MultitableModelAAdmin(models.MultitableModelA, self.site) - mab = MultitableModelBAdmin(models.MultitableModelB, self.site) - - self.assertEqual(tuple(maa.get_form(request).base_fields.keys()), - ('titlea_de', 'titlea_en')) - self.assertEqual(tuple(mab.get_form(request).base_fields.keys()), - ('titlea_de', 'titlea_en', 'titleb_de', 'titleb_en')) - - def test_group_fieldsets(self): - # Declared fieldsets take precedence over group_fieldsets - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - fieldsets = [(None, {'fields': ['title']})] - group_fieldsets = True - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - # Now set group_fieldsets only - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - # Only text and title are registered for translation. We expect to get - # three fieldsets. The first which gathers all untranslated field - # (email only) and one for each translation field (text and title). - fieldsets = [ - ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - # Verify that other options are still taken into account - - # Exclude an untranslated field - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - exclude = ('email',) - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fieldsets = [ - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), - ('text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - # Exclude a translation field - class GroupFieldsetsModelAdmin(admin.TranslationAdmin): - group_fieldsets = True - exclude = ('text',) - ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) - fieldsets = [ - ('', {'fields': ['email']}), - ('title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) - ] - self.assertEqual(ma.get_fieldsets(request), fieldsets) - self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) - - def test_prepopulated_fields(self): - trans_real.activate('de') - self.assertEqual(get_language(), 'de') - - # Non-translated slug based on translated field (using active language) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) - - # Checking multi-field - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname', 'lastname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de', 'lastname_de',)}) - - # Non-translated slug based on non-translated field (no change) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('age',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('age',)}) - - # Translated slug based on non-translated field (all populated on the same value) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug2': ('age',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('age',), 'slug2_de': ('age',)}) - - # Translated slug based on translated field (corresponding) - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug2': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('firstname_en',), - 'slug2_de': ('firstname_de',)}) - - # Check that current active language is used - trans_real.activate('en') - self.assertEqual(get_language(), 'en') - - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_en',)}) - - # Prepopulation language can be overriden by MODELTRANSLATION_PREPOPULATE_LANGUAGE - with reload_override_settings(MODELTRANSLATION_PREPOPULATE_LANGUAGE='de'): - class NameModelAdmin(admin.TranslationAdmin): - prepopulated_fields = {'slug': ('firstname',)} - ma = NameModelAdmin(models.NameModel, self.site) - self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) - - def test_proxymodel_field_argument(self): - class ProxyTestModelAdmin(admin.TranslationAdmin): - fields = ['title'] - - ma = ProxyTestModelAdmin(models.ProxyTestModel, self.site) - fields = ['title_de', 'title_en'] - self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) - self.assertEqual( - tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) - - -class ThirdPartyAppIntegrationTest(ModeltranslationTestBase): - """ - This test case and a test case below have identical tests. The models they test have the same - definition - but in this case the model is not registered for translation and in the other - case it is. - """ - registered = False - - @classmethod - def setUpClass(cls): - # 'model' attribute cannot be assigned to class in its definition, - # because ``models`` module will be reloaded and hence class would use old model classes. - super(ThirdPartyAppIntegrationTest, cls).setUpClass() - cls.model = models.ThirdPartyModel - - def test_form(self): - class CreationForm(forms.ModelForm): - class Meta: - model = self.model - - creation_form = CreationForm({'name': 'abc'}) - inst = creation_form.save() - self.assertEqual('de', get_language()) - self.assertEqual('abc', inst.name) - self.assertEqual(1, self.model.objects.count()) - - -class ThirdPartyAppIntegrationRegisteredTest(ThirdPartyAppIntegrationTest): - registered = True - - @classmethod - def setUpClass(cls): - super(ThirdPartyAppIntegrationRegisteredTest, cls).setUpClass() - cls.model = models.ThirdPartyRegisteredModel - - -class TestManager(ModeltranslationTestBase): - def setUp(self): - # In this test case the default language is en, not de. - super(TestManager, self).setUp() - trans_real.activate('en') - - def test_filter_update(self): - """Test if filtering and updating is language-aware.""" - n = models.ManagerTestModel(title='') - n.title_en = 'en' - n.title_de = 'de' - n.save() - - m = models.ManagerTestModel(title='') - m.title_en = 'title en' - m.title_de = 'de' - m.save() - - self.assertEqual('en', get_language()) - - self.assertEqual(0, models.ManagerTestModel.objects.filter(title='de').count()) - self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en').count()) - # Spanning works - self.assertEqual(2, models.ManagerTestModel.objects.filter(title__contains='en').count()) - - with override('de'): - self.assertEqual(2, models.ManagerTestModel.objects.filter(title='de').count()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(title='en').count()) - # Spanning works - self.assertEqual(2, models.ManagerTestModel.objects.filter(title__endswith='e').count()) - - # Still possible to use explicit language version - self.assertEqual(1, models.ManagerTestModel.objects.filter(title_en='en').count()) - self.assertEqual(2, models.ManagerTestModel.objects.filter( - title_en__contains='en').count()) - - models.ManagerTestModel.objects.update(title='new') - self.assertEqual(2, models.ManagerTestModel.objects.filter(title='new').count()) - n = models.ManagerTestModel.objects.get(pk=n.pk) - m = models.ManagerTestModel.objects.get(pk=m.pk) - self.assertEqual('en', n.title_en) - self.assertEqual('new', n.title_de) - self.assertEqual('title en', m.title_en) - self.assertEqual('new', m.title_de) - - def test_q(self): - """Test if Q queries are rewritten.""" - n = models.ManagerTestModel(title='') - n.title_en = 'en' - n.title_de = 'de' - n.save() - - self.assertEqual('en', get_language()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) - - with override('de'): - self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') - | Q(pk=42)).count()) - self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') - | Q(pk=42)).count()) - - def test_f(self): - """Test if F queries are rewritten.""" - n = models.ManagerTestModel.objects.create(visits_en=1, visits_de=2) - - self.assertEqual('en', get_language()) - models.ManagerTestModel.objects.update(visits=F('visits') + 10) - n = models.ManagerTestModel.objects.all()[0] - self.assertEqual(n.visits_en, 11) - self.assertEqual(n.visits_de, 2) - - with override('de'): - models.ManagerTestModel.objects.update(visits=F('visits') + 20) - n = models.ManagerTestModel.objects.all()[0] - self.assertEqual(n.visits_en, 11) - self.assertEqual(n.visits_de, 22) - - def test_order_by(self): - """Check that field names are rewritten in order_by keys.""" - manager = models.ManagerTestModel.objects - manager.create(title='a') - m = manager.create(title='b') - manager.create(title='c') - with override('de'): - # Make the order of the 'title' column different. - m.title = 'd' - m.save() - titles_asc = tuple(m.title for m in manager.order_by('title')) - titles_desc = tuple(m.title for m in manager.order_by('-title')) - self.assertEqual(titles_asc, ('a', 'b', 'c')) - self.assertEqual(titles_desc, ('c', 'b', 'a')) - - def test_order_by_meta(self): - """Check that meta ordering is rewritten.""" - manager = models.ManagerTestModel.objects - manager.create(title='more_de', visits_en=1, visits_de=2) - manager.create(title='more_en', visits_en=2, visits_de=1) - manager.create(title='most', visits_en=3, visits_de=3) - manager.create(title='least', visits_en=0, visits_de=0) - - # Ordering descending with visits_en - titles_for_en = tuple(m.title_en for m in manager.all()) - with override('de'): - # Ordering descending with visits_de - titles_for_de = tuple(m.title_en for m in manager.all()) - - self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) - self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) - - def test_values(self): - manager = models.ManagerTestModel.objects - manager.create(title_en='en', title_de='de') - - raw_obj = manager.raw_values('title')[0] - obj = manager.values('title')[0] - with override('de'): - raw_obj2 = manager.raw_values('title')[0] - obj2 = manager.values('title')[0] - - # Raw_values returns real database values regardless of current language - self.assertEqual(raw_obj['title'], raw_obj2['title']) - # Values present language-aware data, from the moment of retrieval - self.assertEqual(obj['title'], 'en') - self.assertEqual(obj2['title'], 'de') - - # Values_list behave similarly - self.assertEqual(list(manager.values_list('title', flat=True)), ['en']) - with override('de'): - self.assertEqual(list(manager.values_list('title', flat=True)), ['de']) - - # One can always turn rewrite off - a = list(manager.rewrite(False).values_list('title', flat=True)) - with override('de'): - b = list(manager.rewrite(False).values_list('title', flat=True)) - self.assertEqual(a, b) - - def test_custom_manager(self): - """Test if user-defined manager is still working""" - n = models.CustomManagerTestModel(title='') - n.title_en = 'enigma' - n.title_de = 'foo' - n.save() - - m = models.CustomManagerTestModel(title='') - m.title_en = 'enigma' - m.title_de = 'bar' - m.save() - - # Custom method - self.assertEqual('bar', models.CustomManagerTestModel.objects.foo()) - - # Ensure that get_query_set is working - filter objects to those with 'a' in title - self.assertEqual('en', get_language()) - self.assertEqual(2, models.CustomManagerTestModel.objects.count()) - with override('de'): - self.assertEqual(1, models.CustomManagerTestModel.objects.count()) - - def test_non_objects_manager(self): - """Test if managers other than ``objects`` are patched too""" - from modeltranslation.manager import MultilingualManager - manager = models.CustomManagerTestModel.another_mgr_name - self.assertTrue(isinstance(manager, MultilingualManager)) - - def test_custom_manager2(self): - """Test if user-defined queryset is still working""" - from modeltranslation.manager import MultilingualManager, MultilingualQuerySet - manager = models.CustomManager2TestModel.objects - self.assertTrue(isinstance(manager, models.CustomManager2)) - self.assertTrue(isinstance(manager, MultilingualManager)) - qs = manager.all() - self.assertTrue(isinstance(qs, models.CustomQuerySet)) - self.assertTrue(isinstance(qs, MultilingualQuerySet)) - - def test_creation(self): - """Test if field are rewritten in create.""" - self.assertEqual('en', get_language()) - n = models.ManagerTestModel.objects.create(title='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - # The same result - n = models.ManagerTestModel.objects.create(title_en='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - # Language suffixed version wins - n = models.ManagerTestModel.objects.create(title='bar', title_en='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - def test_creation_population(self): - """Test if language fields are populated with default value on creation.""" - n = models.ManagerTestModel.objects.populate(True).create(title='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual('foo', n.title_de) - self.assertEqual('foo', n.title) - - # You can specify some language... - n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_de='bar') - self.assertEqual('foo', n.title_en) - self.assertEqual('bar', n.title_de) - self.assertEqual('foo', n.title) - - # ... but remember that still original attribute points to current language - self.assertEqual('en', get_language()) - n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_en='bar') - self.assertEqual('bar', n.title_en) - self.assertEqual('foo', n.title_de) - self.assertEqual('bar', n.title) # points to en - with override('de'): - self.assertEqual('foo', n.title) # points to de - self.assertEqual('en', get_language()) - - # This feature (for backward-compatibility) require populate method... - n = models.ManagerTestModel.objects.create(title='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - # ... or MODELTRANSLATION_AUTO_POPULATE setting - with reload_override_settings(MODELTRANSLATION_AUTO_POPULATE=True): - self.assertEqual(True, mt_settings.AUTO_POPULATE) - n = models.ManagerTestModel.objects.create(title='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual('foo', n.title_de) - self.assertEqual('foo', n.title) - - # populate method has highest priority - n = models.ManagerTestModel.objects.populate(False).create(title='foo') - self.assertEqual('foo', n.title_en) - self.assertEqual(None, n.title_de) - self.assertEqual('foo', n.title) - - # Populate ``default`` fills just the default translation. - # TODO: Having more languages would make these tests more meaningful. - qs = models.ManagerTestModel.objects - m = qs.populate('default').create(title='foo', description='bar') - self.assertEqual('foo', m.title_de) - self.assertEqual('foo', m.title_en) - self.assertEqual('bar', m.description_de) - self.assertEqual('bar', m.description_en) - with override('de'): - m = qs.populate('default').create(title='foo', description='bar') - self.assertEqual('foo', m.title_de) - self.assertEqual(None, m.title_en) - self.assertEqual('bar', m.description_de) - self.assertEqual(None, m.description_en) - - # Populate ``required`` fills just non-nullable default translations. - qs = models.ManagerTestModel.objects - m = qs.populate('required').create(title='foo', description='bar') - self.assertEqual('foo', m.title_de) - self.assertEqual('foo', m.title_en) - self.assertEqual(None, m.description_de) - self.assertEqual('bar', m.description_en) - with override('de'): - m = qs.populate('required').create(title='foo', description='bar') - self.assertEqual('foo', m.title_de) - self.assertEqual(None, m.title_en) - self.assertEqual('bar', m.description_de) - self.assertEqual(None, m.description_en) - - def test_get_or_create_population(self): - """ - Populate may be used with ``get_or_create``. - """ - qs = models.ManagerTestModel.objects - m1, created1 = qs.populate(True).get_or_create(title='aaa') - m2, created2 = qs.populate(True).get_or_create(title='aaa') - self.assertTrue(created1) - self.assertFalse(created2) - self.assertEqual(m1, m2) - self.assertEqual('aaa', m1.title_en) - self.assertEqual('aaa', m1.title_de) - - def test_fixture_population(self): - """ - Test that a fixture with values only for the original fields - does not result in missing default translations for (original) - non-nullable fields. - """ - with auto_populate('required'): - call_command('loaddata', 'fixture.json', verbosity=0, commit=False) - m = models.TestModel.objects.get() - self.assertEqual(m.title_en, 'foo') - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.text_en, 'bar') - self.assertEqual(m.text_de, None) - - def test_fixture_population_via_command(self): - """ - Test that the loaddata command takes new option. - """ - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='required') - m = models.TestModel.objects.get() - self.assertEqual(m.title_en, 'foo') - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.text_en, 'bar') - self.assertEqual(m.text_de, None) - - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='all') - m = models.TestModel.objects.get() - self.assertEqual(m.title_en, 'foo') - self.assertEqual(m.title_de, 'foo') - self.assertEqual(m.text_en, 'bar') - self.assertEqual(m.text_de, 'bar') - - # Test if option overrides current context - with auto_populate('all'): - call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate=False) - m = models.TestModel.objects.get() - self.assertEqual(m.title_en, 'foo') - self.assertEqual(m.title_de, None) - self.assertEqual(m.text_en, 'bar') - self.assertEqual(m.text_de, None) - - def assertDeferred(self, use_defer, *fields): - manager = models.TestModel.objects.defer if use_defer else models.TestModel.objects.only - inst1 = manager(*fields)[0] - with override('de'): - inst2 = manager(*fields)[0] - self.assertEqual('title_en', inst1.title) - self.assertEqual('title_en', inst2.title) - with override('de'): - self.assertEqual('title_de', inst1.title) - self.assertEqual('title_de', inst2.title) - - def test_deferred(self): - """ - Check if ``only`` and ``defer`` are working. - """ - models.TestModel.objects.create(title_de='title_de', title_en='title_en') - inst = models.TestModel.objects.only('title_en')[0] - self.assertNotEqual(inst.__class__, models.TestModel) - self.assertTrue(isinstance(inst, models.TestModel)) - self.assertDeferred(False, 'title_en') - - with auto_populate('all'): - self.assertDeferred(False, 'title') - self.assertDeferred(False, 'title_de') - self.assertDeferred(False, 'title_en') - self.assertDeferred(False, 'title_en', 'title_de') - self.assertDeferred(False, 'title', 'title_en') - self.assertDeferred(False, 'title', 'title_de') - # Check if fields are deferred properly with ``only`` - self.assertDeferred(False, 'text') - - # Defer - self.assertDeferred(True, 'title') - self.assertDeferred(True, 'title_de') - self.assertDeferred(True, 'title_en') - self.assertDeferred(True, 'title_en', 'title_de') - self.assertDeferred(True, 'title', 'title_en') - self.assertDeferred(True, 'title', 'title_de') - self.assertDeferred(True, 'text', 'email', 'url') - - def test_constructor_inheritance(self): - inst = models.AbstractModelB() - # Check if fields assigned in constructor hasn't been ignored. - self.assertEqual(inst.titlea, 'title_a') - self.assertEqual(inst.titleb, 'title_b') - - -class TranslationModelFormTest(ModeltranslationTestBase): - def test_fields(self): - class TestModelForm(TranslationModelForm): - class Meta: - model = models.TestModel - - form = TestModelForm() - self.assertEqual(list(form.base_fields), - ['title', 'title_de', 'title_en', 'text', 'text_de', 'text_en', - 'url', 'url_de', 'url_en', 'email', 'email_de', 'email_en']) - self.assertEqual(list(form.fields), ['title', 'text', 'url', 'email']) - - def test_updating_with_empty_value(self): - """ - Can we update the current language translation with an empty value, when - the original field is excluded from the form? - """ - class Form(forms.ModelForm): - class Meta: - model = models.TestModel - exclude = ('text',) - - instance = models.TestModel.objects.create(text_de='something') - form = Form({'text_de': '', 'title': 'a', 'email_de': '', 'email_en': ''}, - instance=instance) - instance = form.save() - self.assertEqual('de', get_language()) - self.assertEqual('', instance.text_de) - - -class ProxyModelTest(ModeltranslationTestBase): - def test_equality(self): - n = models.TestModel.objects.create(title='Title') - m = models.ProxyTestModel.objects.get(title='Title') - self.assertEqual(n.title, m.title) - self.assertEqual(n.title_de, m.title_de) - self.assertEqual(n.title_en, m.title_en) diff --git a/modeltranslation/tests/auth_migrations/__init__.py b/modeltranslation/tests/auth_migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index fd0ab83c..aa281b31 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -16,7 +16,7 @@ class UniqueNullableModel(models.Model): title = models.CharField(null=True, unique=True, max_length=255) -########## Proxy model testing +# ######### Proxy model testing class ProxyTestModel(TestModel): class Meta: @@ -26,7 +26,7 @@ def get_title(self): return self.title -########## Fallback values testing +# ######### Fallback values testing class FallbackModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -43,7 +43,7 @@ class FallbackModel2(models.Model): email = models.EmailField(blank=True, null=True) -########## File fields testing +# ######### File fields testing class FileFieldsModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -52,7 +52,7 @@ class FileFieldsModel(models.Model): image = models.ImageField(upload_to='modeltranslation_tests', null=True, blank=True) -########## Foreign Key / OneToOneField testing +# ######### Foreign Key / OneToOneField testing class NonTranslated(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -60,21 +60,34 @@ class NonTranslated(models.Model): class ForeignKeyModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) - test = models.ForeignKey(TestModel, null=True, related_name="test_fks") - optional = models.ForeignKey(TestModel, blank=True, null=True) - hidden = models.ForeignKey(TestModel, blank=True, null=True, related_name="+") - non = models.ForeignKey(NonTranslated, blank=True, null=True, related_name="test_fks") + test = models.ForeignKey( + TestModel, null=True, related_name="test_fks", on_delete=models.CASCADE, + ) + optional = models.ForeignKey(TestModel, blank=True, null=True, on_delete=models.CASCADE) + hidden = models.ForeignKey( + TestModel, blank=True, null=True, related_name="+", on_delete=models.CASCADE, + ) + non = models.ForeignKey( + NonTranslated, blank=True, null=True, related_name="test_fks", on_delete=models.CASCADE, + ) + untrans = models.ForeignKey( + TestModel, blank=True, null=True, related_name="test_fks_un", on_delete=models.CASCADE, + ) class OneToOneFieldModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) - test = models.OneToOneField(TestModel, null=True, related_name="test_o2o") - optional = models.OneToOneField(TestModel, blank=True, null=True) + test = models.OneToOneField( + TestModel, null=True, related_name="test_o2o", on_delete=models.CASCADE, + ) + optional = models.OneToOneField(TestModel, blank=True, null=True, on_delete=models.CASCADE) # No hidden option for OneToOne - non = models.OneToOneField(NonTranslated, blank=True, null=True, related_name="test_o2o") + non = models.OneToOneField( + NonTranslated, blank=True, null=True, related_name="test_o2o", on_delete=models.CASCADE, + ) -########## Custom fields testing +# ######### Custom fields testing class OtherFieldsModel(models.Model): """ @@ -92,7 +105,7 @@ class OtherFieldsModel(models.Model): date = models.DateField(blank=True, null=True) datetime = models.DateTimeField(blank=True, null=True) time = models.TimeField(blank=True, null=True) -# genericip = models.GenericIPAddressField(blank=True, null=True) + genericip = models.GenericIPAddressField(blank=True, null=True) class FancyDescriptor(object): @@ -139,7 +152,7 @@ class DescriptorModel(models.Model): trans = FancyField() -########## Multitable inheritance testing +# ######### Multitable inheritance testing class MultitableModelA(models.Model): titlea = models.CharField(ugettext_lazy('title a'), max_length=255) @@ -157,7 +170,7 @@ class MultitableModelD(MultitableModelB): titled = models.CharField(ugettext_lazy('title d'), max_length=255) -########## Abstract inheritance testing +# ######### Abstract inheritance testing class AbstractModelA(models.Model): titlea = models.CharField(ugettext_lazy('title a'), max_length=255) @@ -178,7 +191,7 @@ def __init__(self, *args, **kwargs): self.titleb = 'title_b' -########## Fields inheritance testing +# ######### Fields inheritance testing class Slugged(models.Model): slug = models.CharField(max_length=255) @@ -219,7 +232,7 @@ class RichTextPage(Page, RichText): pass -########## Admin testing +# ######### Admin testing class DataModel(models.Model): data = models.TextField(blank=True, null=True) @@ -239,7 +252,7 @@ class NameModel(models.Model): slug2 = models.SlugField(max_length=100) -########## Integration testing +# ######### Integration testing class ThirdPartyModel(models.Model): name = models.CharField(max_length=20) @@ -249,7 +262,7 @@ class ThirdPartyRegisteredModel(models.Model): name = models.CharField(max_length=20) -########## Manager testing +# ######### Manager testing class ManagerTestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) @@ -261,8 +274,16 @@ class Meta: class CustomManager(models.Manager): - def get_query_set(self): - return super(CustomManager, self).get_query_set().filter(title__contains='a') + def get_queryset(self): + sup = super(CustomManager, self) + queryset = sup.get_queryset() if hasattr(sup, 'get_queryset') else sup.get_query_set() + return queryset.filter(title__contains='a').exclude(description__contains='x') + get_query_set = get_queryset + + def custom_qs(self): + sup = super(CustomManager, self) + queryset = sup.get_queryset() if hasattr(sup, 'get_queryset') else sup.get_query_set() + return queryset def foo(self): return 'bar' @@ -270,6 +291,7 @@ def foo(self): class CustomManagerTestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) + description = models.CharField(max_length=255, null=True, db_column='xyz') objects = CustomManager() another_mgr_name = CustomManager() @@ -280,10 +302,110 @@ class CustomQuerySet(models.query.QuerySet): class CustomManager2(models.Manager): - def get_query_set(self): + def get_queryset(self): return CustomQuerySet(self.model, using=self._db) + get_query_set = get_queryset class CustomManager2TestModel(models.Model): title = models.CharField(ugettext_lazy('title'), max_length=255) objects = CustomManager2() + + +# ######### Required fields testing + +class RequiredModel(models.Model): + non_req = models.CharField(max_length=10, blank=True) + req = models.CharField(max_length=10) + req_reg = models.CharField(max_length=10) + req_en_reg = models.CharField(max_length=10) + + +# ######### Decorated registration testing + +class DecoratedModel(models.Model): + title = models.CharField(ugettext_lazy('title'), max_length=255) + + +# ######### Name collision registration testing + +class ConflictModel(models.Model): + title = models.CharField(ugettext_lazy('title'), max_length=255) + title_de = models.IntegerField() + + +class AbstractConflictModelA(models.Model): + title_de = models.IntegerField() + + class Meta: + abstract = True + + +class AbstractConflictModelB(AbstractConflictModelA): + title = models.CharField(ugettext_lazy('title'), max_length=255) + + +class MultitableConflictModelA(models.Model): + title_de = models.IntegerField() + + +class MultitableConflictModelB(MultitableConflictModelA): + title = models.CharField(ugettext_lazy('title'), max_length=255) + + +# ######### Complex M2M with abstract classes and custom managers + +class CustomQuerySetX(models.query.QuerySet): + pass + + +class CustomManagerX(models.Manager): + def get_queryset(self): + return CustomQuerySetX(self.model, using=self._db) + get_query_set = get_queryset + + +class AbstractBaseModelX(models.Model): + name = models.CharField(max_length=255) + objects = CustomManagerX() + + class Meta: + abstract = True + + +class AbstractModelX(AbstractBaseModelX): + class Meta: + abstract = True + + +class ModelX(AbstractModelX): + pass + + +class AbstractModelXY(models.Model): + model_x = models.ForeignKey('ModelX') + model_y = models.ForeignKey('ModelY') + + class Meta: + abstract = True + + +class ModelXY(AbstractModelXY): + pass + + +class CustomManagerY(models.Manager): + pass + + +class AbstractModelY(models.Model): + title = models.CharField(max_length=255) + xs = models.ManyToManyField('ModelX', through='ModelXY') + objects = CustomManagerY() + + class Meta: + abstract = True + + +class ModelY(AbstractModelY): + pass diff --git a/modeltranslation/tests/settings.py b/modeltranslation/tests/settings.py index 9f045199..bb0d6cf2 100644 --- a/modeltranslation/tests/settings.py +++ b/modeltranslation/tests/settings.py @@ -2,17 +2,13 @@ """ Settings overrided for test time """ +import django from django.conf import settings INSTALLED_APPS = tuple(settings.INSTALLED_APPS) + ( 'modeltranslation.tests', ) -# IMO this is unimportant -#if django.VERSION[0] >= 1 and django.VERSION[1] >= 3: - #INSTALLED_APPS += ('django.contrib.staticfiles',) - -#STATIC_URL = '/static/' LANGUAGES = (('de', 'Deutsch'), ('en', 'English')) @@ -21,6 +17,15 @@ USE_I18N = True USE_TZ = False +MIDDLEWARE_CLASSES = () MODELTRANSLATION_AUTO_POPULATE = False MODELTRANSLATION_FALLBACK_LANGUAGES = () + +ROOT_URLCONF = 'modeltranslation.tests.urls' + +if django.VERSION < (1, 11): + # TODO: Check what this was about + MIGRATION_MODULES = {'auth': 'modeltranslation.tests.auth_migrations'} +else: + MIGRATION_MODULES = {} diff --git a/modeltranslation/tests/test_app/models.py b/modeltranslation/tests/test_app/models.py index f8089055..371265ec 100644 --- a/modeltranslation/tests/test_app/models.py +++ b/modeltranslation/tests/test_app/models.py @@ -2,9 +2,13 @@ class News(models.Model): + class Meta: + app_label = 'test_app' title = models.CharField(max_length=50) visits = models.SmallIntegerField(blank=True, null=True) class Other(models.Model): + class Meta: + app_label = 'test_app' name = models.CharField(max_length=50) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py new file mode 100644 index 00000000..bf2dc628 --- /dev/null +++ b/modeltranslation/tests/tests.py @@ -0,0 +1,3120 @@ +# -*- coding: utf-8 -*- +import datetime +from decimal import Decimal +import imp +import os +import shutil + +import django +from django import forms +from django.conf import settings as django_settings +from django.contrib.admin.sites import AdminSite +from django.core.exceptions import ValidationError, ImproperlyConfigured +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.core.management import call_command +from django.db import IntegrityError +from django.db.models import Q, F, Count +from django.test import TestCase, TransactionTestCase +from django.test.utils import override_settings +from django.utils import six +from django.utils.translation import get_language, override, trans_real + +try: + from django.apps import apps as django_apps + NEW_APP_CACHE = True +except ImportError: + from django.db.models.loading import AppCache + NEW_APP_CACHE = False + +try: + from unittest import skipUnless +except ImportError: + # Dummy replacement for Python 2.6 + def skipUnless(condition, reason): + if not condition: + def decorator(test_item): + return lambda s: 42 + return decorator + return lambda x: x # identity + +from modeltranslation import admin, settings as mt_settings, translator +from modeltranslation.forms import TranslationModelForm +from modeltranslation.models import autodiscover +from modeltranslation.tests.test_settings import TEST_SETTINGS +from modeltranslation.utils import (build_css_class, build_localized_fieldname, + auto_populate, fallbacks) + +MIGRATE_CMD = django.VERSION >= (1, 8) +MIGRATIONS = MIGRATE_CMD and "django.contrib.auth" in TEST_SETTINGS['INSTALLED_APPS'] +NEW_DEFERRED_API = django.VERSION >= (1, 10) + +models = translation = None + +# None of the following tests really depend on the content of the request, +# so we'll just pass in None. +request = None + +# How many models are registered for tests. +TEST_MODELS = 31 + (1 if MIGRATIONS else 0) + + +class reload_override_settings(override_settings): + """Context manager that not only override settings, but also reload modeltranslation conf.""" + def __enter__(self): + super(reload_override_settings, self).__enter__() + imp.reload(mt_settings) + + def __exit__(self, exc_type, exc_value, traceback): + super(reload_override_settings, self).__exit__(exc_type, exc_value, traceback) + imp.reload(mt_settings) + + +# In this test suite fallback language is turned off. This context manager temporarily turns it on. +def default_fallback(): + return reload_override_settings( + MODELTRANSLATION_FALLBACK_LANGUAGES=(mt_settings.DEFAULT_LANGUAGE,)) + + +class dummy_context_mgr(): + def __enter__(self): + return None + + def __exit__(self, _type, value, traceback): + return False + + +def get_field_names(model): + if django.VERSION < (1, 9): + return model._meta.get_all_field_names() + names = set() + fields = model._meta.get_fields() + for field in fields: + if field.is_relation and field.many_to_one and field.related_model is None: + continue + if field.model != model and field.model._meta.concrete_model == model._meta.concrete_model: + continue + + names.add(field.name) + if hasattr(field, 'attname'): + names.add(field.attname) + return names + + +@override_settings(**TEST_SETTINGS) +class ModeltranslationTransactionTestBase(TransactionTestCase): + cache = django_apps if NEW_APP_CACHE else AppCache() + synced = False + + @classmethod + def setUpClass(cls): + """ + Prepare database: + * Call syncdb to create tables for tests.models (since during + default testrunner's db creation modeltranslation.tests was not in INSTALLED_APPS + """ + super(ModeltranslationTransactionTestBase, cls).setUpClass() + if not ModeltranslationTransactionTestBase.synced: + # In order to perform only one syncdb + ModeltranslationTransactionTestBase.synced = True + mgr = (override_settings(**TEST_SETTINGS) if django.VERSION < (1, 8) + else dummy_context_mgr()) + with mgr: + # 0. Render initial migration of auth + from django.db import connections, DEFAULT_DB_ALIAS + if MIGRATIONS: + call_command('makemigrations', 'auth', verbosity=2, interactive=False, + database=connections[DEFAULT_DB_ALIAS].alias) + + # 1. Reload translation in case USE_I18N was False + from django.utils import translation as dj_trans + imp.reload(dj_trans) + + # 2. Reload MT because LANGUAGES likely changed. + imp.reload(mt_settings) + imp.reload(translator) + imp.reload(admin) + + # 3. Reset test models (because autodiscover have already run, those models + # have translation fields, but for languages previously defined. We want + # to be sure that 'de' and 'en' are available) + if not NEW_APP_CACHE: + cls.cache.load_app('modeltranslation.tests') + else: + del cls.cache.all_models['tests'] + if MIGRATIONS: + del cls.cache.all_models['auth'] + import sys + sys.modules.pop('modeltranslation.tests.models', None) + sys.modules.pop('modeltranslation.tests.translation', None) + if MIGRATIONS: + sys.modules.pop('django.contrib.auth.models', None) + tests_args = [] + if django.VERSION < (1, 11): + tests_args = [cls.cache.all_models['tests']] + cls.cache.get_app_config('tests').import_models(*tests_args) + if MIGRATIONS: + auth_args = [] + if django.VERSION < (1, 11): + auth_args = [cls.cache.all_models['auth']] + cls.cache.get_app_config('auth').import_models(*auth_args) + + # 4. Autodiscover + from modeltranslation.models import handle_translation_registrations + handle_translation_registrations() + + # 5. makemigrations (``migrate=False`` in case of south) + if MIGRATIONS: + call_command('makemigrations', 'auth', verbosity=2, interactive=False, + database=connections[DEFAULT_DB_ALIAS].alias) + + # 6. Syncdb (``migrate=False`` in case of south) + cmd = 'migrate' if MIGRATE_CMD else 'syncdb' + call_command(cmd, verbosity=0, migrate=False, interactive=False, run_syncdb=True, + database=connections[DEFAULT_DB_ALIAS].alias, load_initial_data=False) + + # 7. clean migrations + if MIGRATIONS: + import glob + dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "auth_migrations") + for f in glob.glob(dir + "/000?_*.py*"): + os.unlink(f) + + # A rather dirty trick to import models into module namespace, but not before + # tests app has been added into INSTALLED_APPS and loaded + # (that's why this is not imported in normal import section) + global models, translation + from modeltranslation.tests import models, translation # NOQA + + def setUp(self): + self._old_language = get_language() + trans_real.activate('de') + + def tearDown(self): + trans_real.activate(self._old_language) + + +class ModeltranslationTestBase(TestCase, ModeltranslationTransactionTestBase): + pass + + +class TestAutodiscover(ModeltranslationTestBase): + # The way the ``override_settings`` works on ``TestCase`` is wicked; + # it patches ``_pre_setup`` and ``_post_teardown`` methods. + # Because of this, if class B extends class A and both are ``override_settings``'ed, + # class B settings would be overwritten by class A settings (if some keys clash). + # To solve this, override some settings after parents ``_pre_setup`` is called. + def _pre_setup(self): + super(TestAutodiscover, self)._pre_setup() + # Add test_app to INSTALLED_APPS + new_installed_apps = django_settings.INSTALLED_APPS + ('modeltranslation.tests.test_app',) + self.__override = override_settings(INSTALLED_APPS=new_installed_apps) + self.__override.enable() + + def _post_teardown(self): + self.__override.disable() + imp.reload(mt_settings) # restore mt_settings.FALLBACK_LANGUAGES + super(TestAutodiscover, self)._post_teardown() + + @classmethod + def setUpClass(cls): + """Save registry (and restore it after tests).""" + super(TestAutodiscover, cls).setUpClass() + from copy import copy + from modeltranslation.translator import translator + cls.registry_cpy = copy(translator._registry) + + @classmethod + def tearDownClass(cls): + from modeltranslation.translator import translator + translator._registry = cls.registry_cpy + super(TestAutodiscover, cls).tearDownClass() + + def tearDown(self): + import sys + # Rollback model classes + if NEW_APP_CACHE: + del self.cache.all_models['test_app'] + else: + del self.cache.app_models['test_app'] + from .test_app import models + imp.reload(models) + # Delete translation modules from import cache + sys.modules.pop('modeltranslation.tests.test_app.translation', None) + sys.modules.pop('modeltranslation.tests.project_translation', None) + super(TestAutodiscover, self).tearDown() + + def check_news(self): + from .test_app.models import News + fields = dir(News()) + self.assertIn('title', fields) + self.assertIn('title_en', fields) + self.assertIn('title_de', fields) + self.assertIn('visits', fields) + self.assertNotIn('visits_en', fields) + self.assertNotIn('visits_de', fields) + + def check_other(self, present=True): + from .test_app.models import Other + fields = dir(Other()) + self.assertIn('name', fields) + if present: + self.assertIn('name_en', fields) + self.assertIn('name_de', fields) + else: + self.assertNotIn('name_en', fields) + self.assertNotIn('name_de', fields) + + def test_simple(self): + """Check if translation is imported for installed apps.""" + autodiscover() + self.check_news() + self.check_other(present=False) + + @reload_override_settings( + MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.project_translation',) + ) + def test_global(self): + """Check if translation is imported for global translation file.""" + autodiscover() + self.check_news() + self.check_other() + + @reload_override_settings( + MODELTRANSLATION_TRANSLATION_FILES=('modeltranslation.tests.test_app.translation',) + ) + def test_duplication(self): + """Check if there is no problem with duplicated filenames.""" + autodiscover() + self.check_news() + + +class ModeltranslationTest(ModeltranslationTestBase): + """Basic tests for the modeltranslation application.""" + def test_registration(self): + langs = tuple(l[0] for l in django_settings.LANGUAGES) + self.assertEqual(langs, tuple(mt_settings.AVAILABLE_LANGUAGES)) + self.assertEqual(2, len(langs)) + self.assertTrue('de' in langs) + self.assertTrue('en' in langs) + self.assertTrue(translator.translator) + + # Check that all models are registered for translation + self.assertEqual(len(translator.translator.get_registered_models()), TEST_MODELS) + + # Try to unregister a model that is not registered + self.assertRaises(translator.NotRegistered, + translator.translator.unregister, models.BasePage) + + # Try to get options for a model that is not registered + self.assertRaises(translator.NotRegistered, + translator.translator.get_options_for_model, models.ThirdPartyModel) + + # Ensure that a base can't be registered after a subclass. + self.assertRaises(translator.DescendantRegistered, + translator.translator.register, models.BasePage) + + # Or unregistered before it. + self.assertRaises(translator.DescendantRegistered, + translator.translator.unregister, models.Slugged) + + @skipUnless(NEW_DEFERRED_API, "Django 1.10 needed") + def test_registration_field_conflicts(self): + before = len(translator.translator.get_registered_models()) + + # Exception should be raised when conflicting field name detected + self.assertRaises(ValueError, translator.translator.register, + models.ConflictModel, fields=('title',)) + self.assertRaises(ValueError, translator.translator.register, + models.AbstractConflictModelB, fields=('title',)) + self.assertRaises(ValueError, translator.translator.register, + models.MultitableConflictModelB, fields=('title',)) + + # Model should not be registered + self.assertEqual(len(translator.translator.get_registered_models()), before) + + def test_fields(self): + field_names = dir(models.TestModel()) + self.assertTrue('id' in field_names) + self.assertTrue('title' in field_names) + self.assertTrue('title_de' in field_names) + self.assertTrue('title_en' in field_names) + self.assertTrue('text' in field_names) + self.assertTrue('text_de' in field_names) + self.assertTrue('text_en' in field_names) + self.assertTrue('url' in field_names) + self.assertTrue('url_de' in field_names) + self.assertTrue('url_en' in field_names) + self.assertTrue('email' in field_names) + self.assertTrue('email_de' in field_names) + self.assertTrue('email_en' in field_names) + + def test_verbose_name(self): + verbose_name = models.TestModel._meta.get_field('title_de').verbose_name + self.assertEqual(six.text_type(verbose_name), 'title [de]') + + def test_descriptor_introspection(self): + # See Django #8248 + try: + models.TestModel.title + models.TestModel.title.__doc__ + self.assertTrue(True) + except: + self.fail('Descriptor accessed on class should return itself.') + + def test_fields_hashes(self): + opts = models.TestModel._meta + orig = opts.get_field('title') + en = opts.get_field('title_en') + de = opts.get_field('title_de') + # Translation field retain creation_counters + self.assertEqual(orig.creation_counter, en.creation_counter) + self.assertEqual(orig.creation_counter, de.creation_counter) + # But they compare unequal + self.assertNotEqual(orig, en) + self.assertNotEqual(orig, de) + self.assertNotEqual(en, de) + # Their hashes too + self.assertNotEqual(hash(orig), hash(en)) + self.assertNotEqual(hash(orig), hash(de)) + self.assertNotEqual(hash(en), hash(de)) + self.assertEqual(3, len(set([orig, en, de]))) + # TranslationFields can compare equal if they have the same language + de.language = 'en' + self.assertNotEqual(orig, de) + self.assertEqual(en, de) + self.assertEqual(hash(en), hash(de)) + self.assertEqual(2, len(set([orig, en, de]))) + de.language = 'de' + + def test_set_translation(self): + """This test briefly shows main modeltranslation features.""" + self.assertEqual(get_language(), 'de') + title_de = "title de" + title_en = "title en" + + # The original field "title" passed in the constructor is + # populated for the current language field: "title_de". + inst2 = models.TestModel(title=title_de) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) + self.assertEqual(inst2.title_de, title_de) + + # So creating object is language-aware + with override('en'): + inst2 = models.TestModel(title=title_en) + self.assertEqual(inst2.title, title_en) + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, None) + + # Value from original field is presented in current language: + inst2 = models.TestModel(title_de=title_de, title_en=title_en) + self.assertEqual(inst2.title, title_de) + with override('en'): + self.assertEqual(inst2.title, title_en) + + # Changes made via original field affect current language field: + inst2.title = 'foo' + self.assertEqual(inst2.title, 'foo') + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, 'foo') + with override('en'): + inst2.title = 'bar' + self.assertEqual(inst2.title, 'bar') + self.assertEqual(inst2.title_en, 'bar') + self.assertEqual(inst2.title_de, 'foo') + self.assertEqual(inst2.title, 'foo') + + # When conflict, language field wins with original field + inst2 = models.TestModel(title='foo', title_de=title_de, title_en=title_en) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, title_en) + self.assertEqual(inst2.title_de, title_de) + + # Creating model and assigning only one language + inst1 = models.TestModel(title_en=title_en) + # Please note: '' and not None, because descriptor falls back to field default value + self.assertEqual(inst1.title, '') + self.assertEqual(inst1.title_en, title_en) + self.assertEqual(inst1.title_de, None) + # Assign current language value - de + inst1.title = title_de + self.assertEqual(inst1.title, title_de) + self.assertEqual(inst1.title_en, title_en) + self.assertEqual(inst1.title_de, title_de) + inst1.save() + + # Check that the translation fields are correctly saved and provide the + # correct value when retrieving them again. + n = models.TestModel.objects.get(title=title_de) + self.assertEqual(n.title, title_de) + self.assertEqual(n.title_en, title_en) + self.assertEqual(n.title_de, title_de) + + # Queries are also language-aware: + self.assertEqual(1, models.TestModel.objects.filter(title=title_de).count()) + with override('en'): + self.assertEqual(0, models.TestModel.objects.filter(title=title_de).count()) + + def test_fallback_language(self): + # Present what happens if current language field is empty + self.assertEqual(get_language(), 'de') + title_de = "title de" + + # Create model with value in de only... + inst2 = models.TestModel(title=title_de) + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) + self.assertEqual(inst2.title_de, title_de) + + # In this test environment, fallback language is not set. So return value for en + # will be field's default: '' + with override('en'): + self.assertEqual(inst2.title, '') + self.assertEqual(inst2.title_en, None) # Language field access returns real value + + # However, by default FALLBACK_LANGUAGES is set to DEFAULT_LANGUAGE + with default_fallback(): + + # No change here... + self.assertEqual(inst2.title, title_de) + + # ... but for empty en fall back to de + with override('en'): + self.assertEqual(inst2.title, title_de) + self.assertEqual(inst2.title_en, None) # Still real value + + def test_fallback_values_1(self): + """ + If ``fallback_values`` is set to string, all untranslated fields would + return this string. + """ + title1_de = "title de" + n = models.FallbackModel(title=title1_de) + n.save() + n = models.FallbackModel.objects.get(title=title1_de) + self.assertEqual(n.title, title1_de) + trans_real.activate("en") + self.assertEqual(n.title, "fallback") + + def test_fallback_values_2(self): + """ + If ``fallback_values`` is set to ``dict``, all untranslated fields in + ``dict`` would return this mapped value. Fields not in ``dict`` would + return default translation. + """ + title1_de = "title de" + text1_de = "text in german" + n = models.FallbackModel2(title=title1_de, text=text1_de) + n.save() + n = models.FallbackModel2.objects.get(title=title1_de) + trans_real.activate("en") + self.assertEqual(n.title, '') # Falling back to default field value + self.assertEqual( + n.text, + translation.FallbackModel2TranslationOptions.fallback_values['text']) + + def _compare_instances(self, x, y, field): + self.assertEqual(getattr(x, field), getattr(y, field), + "Constructor diff on field %s." % field) + + def _test_constructor(self, keywords): + n = models.TestModel(**keywords) + m = models.TestModel.objects.create(**keywords) + opts = translator.translator.get_options_for_model(models.TestModel) + for base_field, trans_fields in opts.fields.items(): + self._compare_instances(n, m, base_field) + for lang_field in trans_fields: + self._compare_instances(n, m, lang_field.name) + + def test_constructor(self): + """ + Ensure that model constructor behaves exactly the same as objects.create + """ + # test different arguments compositions + keywords = dict( + # original only + title='title', + # both languages + original + email='q@q.qq', email_de='d@d.dd', email_en='e@e.ee', + # both languages without original + text_en='text en', text_de='text de', + ) + self._test_constructor(keywords) + + keywords = dict( + # only current language + title_de='title', + # only not current language + url_en='http://www.google.com', + # original + current + text='text def', text_de='text de', + # original + not current + email='q@q.qq', email_en='e@e.ee', + ) + self._test_constructor(keywords) + + +class ModeltranslationTransactionTest(ModeltranslationTransactionTestBase): + def test_unique_nullable_field(self): + from django.db import transaction + models.UniqueNullableModel.objects.create() + models.UniqueNullableModel.objects.create() + models.UniqueNullableModel.objects.create(title=None) + models.UniqueNullableModel.objects.create(title=None) + + models.UniqueNullableModel.objects.create(title='') + self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='') + transaction.rollback() # Postgres + models.UniqueNullableModel.objects.create(title='foo') + self.assertRaises(IntegrityError, models.UniqueNullableModel.objects.create, title='foo') + transaction.rollback() # Postgres + + +class FallbackTests(ModeltranslationTestBase): + test_fallback = { + 'default': ('de',), + 'de': ('en',) + } + + def test_settings(self): + # Initial + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ()}) + # Tuple/list + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('de',)): + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, {'default': ('de',)}) + # Whole dict + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + self.assertEqual(mt_settings.FALLBACK_LANGUAGES, self.test_fallback) + # Improper language raises error + config = {'default': (), 'fr': ('en',)} + with override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=config): + self.assertRaises(ImproperlyConfigured, lambda: imp.reload(mt_settings)) + imp.reload(mt_settings) + + def test_resolution_order(self): + from modeltranslation.utils import resolution_order + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + self.assertEqual(('en', 'de'), resolution_order('en')) + self.assertEqual(('de', 'en'), resolution_order('de')) + # Overriding + config = {'default': ()} + self.assertEqual(('en',), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + # Uniqueness + config = {'de': ('en', 'de')} + self.assertEqual(('en', 'de'), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + + # Default fallbacks are always used at the end + # That's it: fallbacks specified for a language don't replace defaults, + # but just are prepended + config = {'default': ('en', 'de'), 'de': ()} + self.assertEqual(('en', 'de'), resolution_order('en', config)) + self.assertEqual(('de', 'en'), resolution_order('de', config)) + # What one may have expected + self.assertNotEqual(('de',), resolution_order('de', config)) + + # To completely override settings, one should override all keys + config = {'default': (), 'de': ()} + self.assertEqual(('en',), resolution_order('en', config)) + self.assertEqual(('de',), resolution_order('de', config)) + + def test_fallback_languages(self): + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + title_de = 'title de' + title_en = 'title en' + n = models.TestModel(title=title_de) + self.assertEqual(n.title_de, title_de) + self.assertEqual(n.title_en, None) + self.assertEqual(n.title, title_de) + trans_real.activate('en') + self.assertEqual(n.title, title_de) # since default fallback is de + + n = models.TestModel(title=title_en) + self.assertEqual(n.title_de, None) + self.assertEqual(n.title_en, title_en) + self.assertEqual(n.title, title_en) + trans_real.activate('de') + self.assertEqual(n.title, title_en) # since fallback for de is en + + n.title_en = None + self.assertEqual(n.title, '') # if all fallbacks fail, return field.get_default() + + def test_fallbacks_toggle(self): + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + m = models.TestModel(title='foo') + with fallbacks(True): + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.title_en, None) + self.assertEqual(m.title, 'foo') + with override('en'): + self.assertEqual(m.title, 'foo') + with fallbacks(False): + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.title_en, None) + self.assertEqual(m.title, 'foo') + with override('en'): + self.assertEqual(m.title, '') # '' is the default + + def test_fallback_undefined(self): + """ + Checks if a sensible value is considered undefined and triggers + fallbacks. Tests if the value can be overridden as documented. + """ + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=self.test_fallback): + # Non-nullable CharField falls back on empty strings. + m = models.FallbackModel(title_en='value', title_de='') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, 'value') + + # Nullable CharField does not fall back on empty strings. + m = models.FallbackModel(description_en='value', description_de='') + with override('en'): + self.assertEqual(m.description, 'value') + with override('de'): + self.assertEqual(m.description, '') + + # Nullable CharField does fall back on None. + m = models.FallbackModel(description_en='value', description_de=None) + with override('en'): + self.assertEqual(m.description, 'value') + with override('de'): + self.assertEqual(m.description, 'value') + + # The undefined value may be overridden. + m = models.FallbackModel2(title_en='value', title_de='') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, '') + m = models.FallbackModel2(title_en='value', title_de='no title') + with override('en'): + self.assertEqual(m.title, 'value') + with override('de'): + self.assertEqual(m.title, 'value') + + +class FileFieldsTest(ModeltranslationTestBase): + + def tearDown(self): + if default_storage.exists('modeltranslation_tests'): + # With FileSystemStorage uploading files creates a new directory, + # that's not automatically removed upon their deletion. + tests_dir = default_storage.path('modeltranslation_tests') + if os.path.isdir(tests_dir): + shutil.rmtree(tests_dir) + super(FileFieldsTest, self).tearDown() + + def test_translated_models(self): + field_names = dir(models.FileFieldsModel()) + self.assertTrue('id' in field_names) + self.assertTrue('title' in field_names) + self.assertTrue('title_de' in field_names) + self.assertTrue('title_en' in field_names) + self.assertTrue('file' in field_names) + self.assertTrue('file_de' in field_names) + self.assertTrue('file_en' in field_names) + self.assertTrue('image' in field_names) + self.assertTrue('image_de' in field_names) + self.assertTrue('image_en' in field_names) + + def _file_factory(self, name, content): + try: + return ContentFile(content, name=name) + except TypeError: # In Django 1.3 ContentFile had no name parameter + file = ContentFile(content) + file.name = name + return file + + def test_translated_models_instance(self): + inst = models.FileFieldsModel(title="Testtitle") + + trans_real.activate("en") + inst.title = 'title_en' + inst.file = 'a_en' + inst.file.save('b_en', ContentFile('file in english')) + inst.image = self._file_factory('i_en.jpg', 'image in english') # Direct assign + + trans_real.activate("de") + inst.title = 'title_de' + inst.file = 'a_de' + inst.file.save('b_de', ContentFile('file in german')) + inst.image = self._file_factory('i_de.jpg', 'image in german') + + inst.save() + + trans_real.activate("en") + self.assertEqual(inst.title, 'title_en') + self.assertTrue(inst.file.name.count('b_en') > 0) + self.assertEqual(inst.file.read(), b'file in english') + self.assertTrue(inst.image.name.count('i_en') > 0) + self.assertEqual(inst.image.read(), b'image in english') + + # Check if file was actually created in the global storage. + self.assertTrue(default_storage.exists(inst.file)) + self.assertTrue(inst.file.size > 0) + self.assertTrue(default_storage.exists(inst.image)) + self.assertTrue(inst.image.size > 0) + + trans_real.activate("de") + self.assertEqual(inst.title, 'title_de') + self.assertTrue(inst.file.name.count('b_de') > 0) + self.assertEqual(inst.file.read(), b'file in german') + self.assertTrue(inst.image.name.count('i_de') > 0) + self.assertEqual(inst.image.read(), b'image in german') + + inst.file_en.delete() + inst.image_en.delete() + inst.file_de.delete() + inst.image_de.delete() + + def test_empty_field(self): + from django.db.models.fields.files import FieldFile + inst = models.FileFieldsModel() + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + inst.save() + inst = models.FileFieldsModel.objects.all()[0] + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + + def test_fallback(self): + from django.db.models.fields.files import FieldFile + with reload_override_settings(MODELTRANSLATION_FALLBACK_LANGUAGES=('en',)): + self.assertEqual(get_language(), 'de') + inst = models.FileFieldsModel() + inst.file_de = '' + inst.file_en = 'foo' + inst.file2_de = '' + inst.file2_en = 'bar' + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + self.assertEqual(inst.file.name, 'foo') + self.assertEqual(inst.file2.name, 'bar') + inst.save() + inst = models.FileFieldsModel.objects.all()[0] + self.assertIsInstance(inst.file, FieldFile) + self.assertIsInstance(inst.file2, FieldFile) + self.assertEqual(inst.file.name, 'foo') + self.assertEqual(inst.file2.name, 'bar') + + +class ForeignKeyFieldsTest(ModeltranslationTestBase): + @classmethod + def setUpClass(cls): + # 'model' attribute cannot be assigned to class in its definition, + # because ``models`` module will be reloaded and hence class would use old model classes. + super(ForeignKeyFieldsTest, cls).setUpClass() + cls.model = models.ForeignKeyModel + + def test_translated_models(self): + field_names = dir(self.model()) + self.assertTrue('id' in field_names) + for f in ('test', 'test_de', 'test_en', 'optional', 'optional_en', 'optional_de'): + self.assertTrue(f in field_names) + self.assertTrue('%s_id' % f in field_names) + + def test_db_column_names(self): + meta = self.model._meta + + # Make sure the correct database columns always get used: + attname, col = meta.get_field('test').get_attname_column() + self.assertEqual(attname, 'test_id') + self.assertEqual(attname, col) + + attname, col = meta.get_field('test_en').get_attname_column() + self.assertEqual(attname, 'test_en_id') + self.assertEqual(attname, col) + + attname, col = meta.get_field('test_de').get_attname_column() + self.assertEqual(attname, 'test_de_id') + self.assertEqual(attname, col) + + def test_translated_models_instance(self): + test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') + test_inst1.save() + test_inst2 = models.TestModel(title_en='title2_en', title_de='title2_de') + test_inst2.save() + inst = self.model() + + trans_real.activate("de") + inst.test = test_inst1 + inst.optional = None + + trans_real.activate("en") + # Test assigning relation by ID: + inst.optional_id = test_inst2.pk + inst.save() + + trans_real.activate("de") + self.assertEqual(inst.test_id, test_inst1.pk) + self.assertEqual(inst.test.title, 'title1_de') + self.assertEqual(inst.test_de_id, test_inst1.pk) + self.assertEqual(inst.test_de.title, 'title1_de') + self.assertEqual(inst.optional, None) + + # Test fallbacks: + trans_real.activate("en") + with default_fallback(): + self.assertEqual(inst.test_id, test_inst1.pk) + self.assertEqual(inst.test.pk, test_inst1.pk) + self.assertEqual(inst.test.title, 'title1_en') + + # Test English: + self.assertEqual(inst.optional_id, test_inst2.pk) + self.assertEqual(inst.optional.title, 'title2_en') + self.assertEqual(inst.optional_en_id, test_inst2.pk) + self.assertEqual(inst.optional_en.title, 'title2_en') + + # Test caching + inst.test_en = test_inst2 + inst.save() + trans_real.activate("de") + self.assertEqual(inst.test, test_inst1) + trans_real.activate("en") + self.assertEqual(inst.test, test_inst2) + + # Check filtering in direct way + lookup spanning + manager = self.model.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test=test_inst1).count(), 1) + self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) + self.assertEqual(manager.filter(test=test_inst2).count(), 0) + self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) + self.assertEqual(manager.filter(test__title='title1_de').count(), 1) + self.assertEqual(manager.filter(test__title='title1_en').count(), 0) + self.assertEqual(manager.filter(test__title_en='title1_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_en=test_inst1).count(), 0) + self.assertEqual(manager.filter(test_de=test_inst1).count(), 1) + self.assertEqual(manager.filter(test=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_en=test_inst2).count(), 1) + self.assertEqual(manager.filter(test_de=test_inst2).count(), 0) + self.assertEqual(manager.filter(test__title='title2_en').count(), 1) + self.assertEqual(manager.filter(test__title='title2_de').count(), 0) + self.assertEqual(manager.filter(test__title_de='title2_de').count(), 1) + + def test_reverse_relations(self): + test_inst = models.TestModel(title_en='title_en', title_de='title_de') + test_inst.save() + + # Instantiate many 'ForeignKeyModel' instances: + fk_inst_both = self.model(title_en='f_title_en', title_de='f_title_de', + test_de=test_inst, test_en=test_inst) + fk_inst_both.save() + fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de', + test_de_id=test_inst.pk) + fk_inst_de.save() + fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de', + test_en=test_inst) + fk_inst_en.save() + + fk_option_de = self.model.objects.create(optional_de=test_inst) + fk_option_en = self.model.objects.create(optional_en=test_inst) + + # Check that the reverse accessors are created on the model: + # Explicit related_name + testmodel_fields = get_field_names(models.TestModel) + testmodel_methods = dir(models.TestModel) + self.assertIn('test_fks', testmodel_fields) + self.assertIn('test_fks_de', testmodel_fields) + self.assertIn('test_fks_en', testmodel_fields) + self.assertIn('test_fks', testmodel_methods) + self.assertIn('test_fks_de', testmodel_methods) + self.assertIn('test_fks_en', testmodel_methods) + # Implicit related_name: manager descriptor name != query field name + self.assertIn('foreignkeymodel', testmodel_fields) + self.assertIn('foreignkeymodel_de', testmodel_fields) + self.assertIn('foreignkeymodel_en', testmodel_fields) + self.assertIn('foreignkeymodel_set', testmodel_methods) + self.assertIn('foreignkeymodel_set_de', testmodel_methods) + self.assertIn('foreignkeymodel_set_en', testmodel_methods) + + # Check the German reverse accessor: + self.assertIn(fk_inst_both, test_inst.test_fks_de.all()) + self.assertIn(fk_inst_de, test_inst.test_fks_de.all()) + self.assertNotIn(fk_inst_en, test_inst.test_fks_de.all()) + + # Check the English reverse accessor: + self.assertIn(fk_inst_both, test_inst.test_fks_en.all()) + self.assertIn(fk_inst_en, test_inst.test_fks_en.all()) + self.assertNotIn(fk_inst_de, test_inst.test_fks_en.all()) + + # Check the default reverse accessor: + trans_real.activate("de") + self.assertIn(fk_inst_de, test_inst.test_fks.all()) + self.assertNotIn(fk_inst_en, test_inst.test_fks.all()) + trans_real.activate("en") + self.assertIn(fk_inst_en, test_inst.test_fks.all()) + self.assertNotIn(fk_inst_de, test_inst.test_fks.all()) + + # Check implicit related_name reverse accessor: + self.assertIn(fk_option_en, test_inst.foreignkeymodel_set.all()) + + # Check filtering in reverse way + lookup spanning: + manager = models.TestModel.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks__id=fk_inst_de.pk).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 0) + self.assertEqual(manager.filter(foreignkeymodel_en=fk_option_en).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 0) + self.assertEqual(manager.filter(test_fks__title_en='f_title_en').distinct().count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks__id=fk_inst_en.pk).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_en).count(), 1) + self.assertEqual(manager.filter(foreignkeymodel=fk_option_de).count(), 0) + self.assertEqual(manager.filter(foreignkeymodel_de=fk_option_de).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').distinct().count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').distinct().count(), 0) + self.assertEqual(manager.filter(test_fks__title_de='f_title_de').distinct().count(), 1) + + # Check assignment + trans_real.activate("de") + test_inst2 = models.TestModel(title_en='title_en', title_de='title_de') + test_inst2.save() + if django.VERSION >= (1, 9): + test_inst2.test_fks.set((fk_inst_de, fk_inst_both)) + test_inst2.test_fks_en.set((fk_inst_en, fk_inst_both)) + else: + test_inst2.test_fks = [fk_inst_de, fk_inst_both] + test_inst2.test_fks_en = (fk_inst_en, fk_inst_both) + self.assertEqual(fk_inst_both.test.pk, test_inst2.pk) + self.assertEqual(fk_inst_both.test_id, test_inst2.pk) + self.assertEqual(fk_inst_both.test_de, test_inst2) + self.assertQuerysetsEqual(test_inst2.test_fks_de.all(), test_inst2.test_fks.all()) + self.assertIn(fk_inst_both, test_inst2.test_fks.all()) + self.assertIn(fk_inst_de, test_inst2.test_fks.all()) + self.assertNotIn(fk_inst_en, test_inst2.test_fks.all()) + trans_real.activate("en") + self.assertQuerysetsEqual(test_inst2.test_fks_en.all(), test_inst2.test_fks.all()) + self.assertIn(fk_inst_both, test_inst2.test_fks.all()) + self.assertIn(fk_inst_en, test_inst2.test_fks.all()) + self.assertNotIn(fk_inst_de, test_inst2.test_fks.all()) + + def test_non_translated_relation(self): + non_de = models.NonTranslated.objects.create(title='title_de') + non_en = models.NonTranslated.objects.create(title='title_en') + + fk_inst_both = self.model.objects.create( + title_en='f_title_en', title_de='f_title_de', non_de=non_de, non_en=non_en) + fk_inst_de = self.model.objects.create(non_de=non_de) + fk_inst_en = self.model.objects.create(non_en=non_en) + + # Forward relation + spanning + manager = self.model.objects + trans_real.activate("de") + self.assertEqual(manager.filter(non=non_de).count(), 2) + self.assertEqual(manager.filter(non=non_en).count(), 0) + self.assertEqual(manager.filter(non_en=non_en).count(), 2) + self.assertEqual(manager.filter(non__title='title_de').count(), 2) + self.assertEqual(manager.filter(non__title='title_en').count(), 0) + self.assertEqual(manager.filter(non_en__title='title_en').count(), 2) + trans_real.activate("en") + self.assertEqual(manager.filter(non=non_en).count(), 2) + self.assertEqual(manager.filter(non=non_de).count(), 0) + self.assertEqual(manager.filter(non_de=non_de).count(), 2) + self.assertEqual(manager.filter(non__title='title_en').count(), 2) + self.assertEqual(manager.filter(non__title='title_de').count(), 0) + self.assertEqual(manager.filter(non_de__title='title_de').count(), 2) + + # Reverse relation + spanning + manager = models.NonTranslated.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_fks_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 0) + self.assertEqual(manager.filter(test_fks__title_en='f_title_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_fks=fk_inst_both).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_fks=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_fks_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_en').count(), 1) + self.assertEqual(manager.filter(test_fks__title='f_title_de').count(), 0) + self.assertEqual(manager.filter(test_fks__title_de='f_title_de').count(), 1) + + def test_indonesian(self): + field = models.ForeignKeyModel._meta.get_field('test') + self.assertNotEqual(field.attname, build_localized_fieldname(field.name, 'id')) + + def assertQuerysetsEqual(self, qs1, qs2): + def pk(o): + return o.pk + + return self.assertEqual(sorted(qs1, key=pk), sorted(qs2, key=pk)) + + +class OneToOneFieldsTest(ForeignKeyFieldsTest): + @classmethod + def setUpClass(cls): + # 'model' attribute cannot be assigned to class in its definition, + # because ``models`` module will be reloaded and hence class would use old model classes. + super(OneToOneFieldsTest, cls).setUpClass() + cls.model = models.OneToOneFieldModel + + def test_uniqueness(self): + test_inst1 = models.TestModel(title_en='title1_en', title_de='title1_de') + test_inst1.save() + inst = self.model() + + trans_real.activate("de") + inst.test = test_inst1 + + trans_real.activate("en") + # That's ok, since test_en is different than test_de + inst.test = test_inst1 + inst.save() + + # But this violates uniqueness constraint + inst2 = self.model(test=test_inst1) + self.assertRaises(IntegrityError, inst2.save) + + def test_reverse_relations(self): + test_inst = models.TestModel(title_en='title_en', title_de='title_de') + test_inst.save() + + # Instantiate many 'OneToOneFieldModel' instances: + fk_inst_de = self.model(title_en='f_title_en', title_de='f_title_de', + test_de_id=test_inst.pk) + fk_inst_de.save() + fk_inst_en = self.model(title_en='f_title_en', title_de='f_title_de', + test_en=test_inst) + fk_inst_en.save() + + fk_option_de = self.model.objects.create(optional_de=test_inst) + fk_option_en = self.model.objects.create(optional_en=test_inst) + + # Check that the reverse accessors are created on the model: + # Explicit related_name + testmodel_fields = get_field_names(models.TestModel) + testmodel_methods = dir(models.TestModel) + self.assertIn('test_o2o', testmodel_fields) + self.assertIn('test_o2o_de', testmodel_fields) + self.assertIn('test_o2o_en', testmodel_fields) + self.assertIn('test_o2o', testmodel_methods) + self.assertIn('test_o2o_de', testmodel_methods) + self.assertIn('test_o2o_en', testmodel_methods) + # Implicit related_name + self.assertIn('onetoonefieldmodel', testmodel_fields) + self.assertIn('onetoonefieldmodel_de', testmodel_fields) + self.assertIn('onetoonefieldmodel_en', testmodel_fields) + self.assertIn('onetoonefieldmodel', testmodel_methods) + self.assertIn('onetoonefieldmodel_de', testmodel_methods) + self.assertIn('onetoonefieldmodel_en', testmodel_methods) + + # Check the German reverse accessor: + self.assertEqual(fk_inst_de, test_inst.test_o2o_de) + + # Check the English reverse accessor: + self.assertEqual(fk_inst_en, test_inst.test_o2o_en) + + # Check the default reverse accessor: + trans_real.activate("de") + self.assertEqual(fk_inst_de, test_inst.test_o2o) + trans_real.activate("en") + self.assertEqual(fk_inst_en, test_inst.test_o2o) + + # Check implicit related_name reverse accessor: + self.assertEqual(fk_option_en, test_inst.onetoonefieldmodel) + + # Check filtering in reverse way + lookup spanning: + manager = models.TestModel.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_o2o__id=fk_inst_de.pk).count(), 1) + self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 1) + self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 0) + self.assertEqual(manager.filter(onetoonefieldmodel_en=fk_option_en).count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 0) + self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').distinct().count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_o2o__id=fk_inst_en.pk).count(), 1) + self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_en).count(), 1) + self.assertEqual(manager.filter(onetoonefieldmodel=fk_option_de).count(), 0) + self.assertEqual(manager.filter(onetoonefieldmodel_de=fk_option_de).count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_en').distinct().count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_de').distinct().count(), 0) + self.assertEqual(manager.filter(test_o2o__title_de='f_title_de').distinct().count(), 1) + + # Check assignment + trans_real.activate("de") + test_inst2 = models.TestModel(title_en='title_en', title_de='title_de') + test_inst2.save() + test_inst2.test_o2o = fk_inst_de + test_inst2.test_o2o_en = fk_inst_en + + self.assertEqual(fk_inst_de.test.pk, test_inst2.pk) + self.assertEqual(fk_inst_de.test_id, test_inst2.pk) + self.assertEqual(fk_inst_de.test_de, test_inst2) + self.assertEqual(test_inst2.test_o2o_de, test_inst2.test_o2o) + self.assertEqual(fk_inst_de, test_inst2.test_o2o) + trans_real.activate("en") + self.assertEqual(fk_inst_en.test.pk, test_inst2.pk) + self.assertEqual(fk_inst_en.test_id, test_inst2.pk) + self.assertEqual(fk_inst_en.test_en, test_inst2) + self.assertEqual(test_inst2.test_o2o_en, test_inst2.test_o2o) + self.assertEqual(fk_inst_en, test_inst2.test_o2o) + + def test_non_translated_relation(self): + non_de = models.NonTranslated.objects.create(title='title_de') + non_en = models.NonTranslated.objects.create(title='title_en') + + fk_inst_de = self.model.objects.create( + title_en='f_title_en', title_de='f_title_de', non_de=non_de) + fk_inst_en = self.model.objects.create( + title_en='f_title_en2', title_de='f_title_de2', non_en=non_en) + + # Forward relation + spanning + manager = self.model.objects + trans_real.activate("de") + self.assertEqual(manager.filter(non=non_de).count(), 1) + self.assertEqual(manager.filter(non=non_en).count(), 0) + self.assertEqual(manager.filter(non_en=non_en).count(), 1) + self.assertEqual(manager.filter(non__title='title_de').count(), 1) + self.assertEqual(manager.filter(non__title='title_en').count(), 0) + self.assertEqual(manager.filter(non_en__title='title_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(non=non_en).count(), 1) + self.assertEqual(manager.filter(non=non_de).count(), 0) + self.assertEqual(manager.filter(non_de=non_de).count(), 1) + self.assertEqual(manager.filter(non__title='title_en').count(), 1) + self.assertEqual(manager.filter(non__title='title_de').count(), 0) + self.assertEqual(manager.filter(non_de__title='title_de').count(), 1) + + # Reverse relation + spanning + manager = models.NonTranslated.objects + trans_real.activate("de") + self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 0) + self.assertEqual(manager.filter(test_o2o_en=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_de').count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_en').count(), 0) + self.assertEqual(manager.filter(test_o2o__title_en='f_title_en').count(), 1) + trans_real.activate("en") + self.assertEqual(manager.filter(test_o2o=fk_inst_en).count(), 1) + self.assertEqual(manager.filter(test_o2o=fk_inst_de).count(), 0) + self.assertEqual(manager.filter(test_o2o_de=fk_inst_de).count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_en2').count(), 1) + self.assertEqual(manager.filter(test_o2o__title='f_title_de2').count(), 0) + self.assertEqual(manager.filter(test_o2o__title_de='f_title_de2').count(), 1) + + +class OtherFieldsTest(ModeltranslationTestBase): + def test_translated_models(self): + inst = models.OtherFieldsModel.objects.create() + field_names = dir(inst) + self.assertTrue('id' in field_names) + self.assertTrue('int' in field_names) + self.assertTrue('int_de' in field_names) + self.assertTrue('int_en' in field_names) + self.assertTrue('boolean' in field_names) + self.assertTrue('boolean_de' in field_names) + self.assertTrue('boolean_en' in field_names) + self.assertTrue('nullboolean' in field_names) + self.assertTrue('nullboolean_de' in field_names) + self.assertTrue('nullboolean_en' in field_names) + self.assertTrue('csi' in field_names) + self.assertTrue('csi_de' in field_names) + self.assertTrue('csi_en' in field_names) + self.assertTrue('ip' in field_names) + self.assertTrue('ip_de' in field_names) + self.assertTrue('ip_en' in field_names) + self.assertTrue('genericip' in field_names) + self.assertTrue('genericip_de' in field_names) + self.assertTrue('genericip_en' in field_names) + self.assertTrue('float' in field_names) + self.assertTrue('float_de' in field_names) + self.assertTrue('float_en' in field_names) + self.assertTrue('decimal' in field_names) + self.assertTrue('decimal_de' in field_names) + self.assertTrue('decimal_en' in field_names) + inst.delete() + + def test_translated_models_integer_instance(self): + inst = models.OtherFieldsModel() + inst.int = 7 + self.assertEqual('de', get_language()) + self.assertEqual(7, inst.int) + self.assertEqual(7, inst.int_de) + self.assertEqual(42, inst.int_en) # default value is honored + + inst.int += 2 + inst.save() + self.assertEqual(9, inst.int) + self.assertEqual(9, inst.int_de) + self.assertEqual(42, inst.int_en) + + trans_real.activate('en') + inst.int -= 1 + self.assertEqual(41, inst.int) + self.assertEqual(9, inst.int_de) + self.assertEqual(41, inst.int_en) + + # this field has validator - let's try to make it below 0! + inst.int -= 50 + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_boolean_instance(self): + inst = models.OtherFieldsModel() + inst.boolean = True + self.assertEqual('de', get_language()) + self.assertEqual(True, inst.boolean) + self.assertEqual(True, inst.boolean_de) + self.assertEqual(False, inst.boolean_en) + + inst.boolean = False + inst.save() + self.assertEqual(False, inst.boolean) + self.assertEqual(False, inst.boolean_de) + self.assertEqual(False, inst.boolean_en) + + trans_real.activate('en') + inst.boolean = True + self.assertEqual(True, inst.boolean) + self.assertEqual(False, inst.boolean_de) + self.assertEqual(True, inst.boolean_en) + + def test_translated_models_nullboolean_instance(self): + inst = models.OtherFieldsModel() + inst.nullboolean = True + self.assertEqual('de', get_language()) + self.assertEqual(True, inst.nullboolean) + self.assertEqual(True, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + inst.nullboolean = False + inst.save() + self.assertEqual(False, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + trans_real.activate('en') + inst.nullboolean = True + self.assertEqual(True, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(True, inst.nullboolean_en) + + inst.nullboolean = None + self.assertEqual(None, inst.nullboolean) + self.assertEqual(False, inst.nullboolean_de) + self.assertEqual(None, inst.nullboolean_en) + + def test_translated_models_commaseparatedinteger_instance(self): + inst = models.OtherFieldsModel() + inst.csi = '4,8,15,16,23,42' + self.assertEqual('de', get_language()) + self.assertEqual('4,8,15,16,23,42', inst.csi) + self.assertEqual('4,8,15,16,23,42', inst.csi_de) + self.assertEqual(None, inst.csi_en) + + inst.csi = '23,42' + inst.save() + self.assertEqual('23,42', inst.csi) + self.assertEqual('23,42', inst.csi_de) + self.assertEqual(None, inst.csi_en) + + trans_real.activate('en') + inst.csi = '4,8,15,16,23,42' + self.assertEqual('4,8,15,16,23,42', inst.csi) + self.assertEqual('23,42', inst.csi_de) + self.assertEqual('4,8,15,16,23,42', inst.csi_en) + + # Now that we have covered csi, lost, illuminati and hitchhiker + # compliance in a single test, do something useful... + + # Check if validation is preserved + inst.csi = '1;2' + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_ipaddress_instance(self): + inst = models.OtherFieldsModel() + inst.ip = '192.0.1.42' + self.assertEqual('de', get_language()) + self.assertEqual('192.0.1.42', inst.ip) + self.assertEqual('192.0.1.42', inst.ip_de) + self.assertEqual(None, inst.ip_en) + + inst.ip = '192.0.23.1' + inst.save() + self.assertEqual('192.0.23.1', inst.ip) + self.assertEqual('192.0.23.1', inst.ip_de) + self.assertEqual(None, inst.ip_en) + + trans_real.activate('en') + inst.ip = '192.0.1.42' + self.assertEqual('192.0.1.42', inst.ip) + self.assertEqual('192.0.23.1', inst.ip_de) + self.assertEqual('192.0.1.42', inst.ip_en) + + # Check if validation is preserved + inst.ip = '1;2' + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_genericipaddress_instance(self): + inst = models.OtherFieldsModel() + inst.genericip = '2a02:42fe::4' + self.assertEqual('de', get_language()) + self.assertEqual('2a02:42fe::4', inst.genericip) + self.assertEqual('2a02:42fe::4', inst.genericip_de) + self.assertEqual(None, inst.genericip_en) + + inst.genericip = '2a02:23fe::4' + inst.save() + self.assertEqual('2a02:23fe::4', inst.genericip) + self.assertEqual('2a02:23fe::4', inst.genericip_de) + self.assertEqual(None, inst.genericip_en) + + trans_real.activate('en') + inst.genericip = '2a02:42fe::4' + self.assertEqual('2a02:42fe::4', inst.genericip) + self.assertEqual('2a02:23fe::4', inst.genericip_de) + self.assertEqual('2a02:42fe::4', inst.genericip_en) + + # Check if validation is preserved + inst.genericip = '1;2' + self.assertRaises(ValidationError, inst.full_clean) + + def test_translated_models_float_instance(self): + inst = models.OtherFieldsModel() + inst.float = 0.42 + self.assertEqual('de', get_language()) + self.assertEqual(0.42, inst.float) + self.assertEqual(0.42, inst.float_de) + self.assertEqual(None, inst.float_en) + + inst.float = 0.23 + inst.save() + self.assertEqual(0.23, inst.float) + self.assertEqual(0.23, inst.float_de) + self.assertEqual(None, inst.float_en) + + inst.float += 0.08 + self.assertEqual(0.31, inst.float) + self.assertEqual(0.31, inst.float_de) + self.assertEqual(None, inst.float_en) + + trans_real.activate('en') + inst.float = 0.42 + self.assertEqual(0.42, inst.float) + self.assertEqual(0.31, inst.float_de) + self.assertEqual(0.42, inst.float_en) + + def test_translated_models_decimal_instance(self): + inst = models.OtherFieldsModel() + inst.decimal = Decimal('0.42') + self.assertEqual('de', get_language()) + self.assertEqual(Decimal('0.42'), inst.decimal) + self.assertEqual(Decimal('0.42'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + inst.decimal = inst.decimal - Decimal('0.19') + inst.save() + self.assertEqual(Decimal('0.23'), inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + trans_real.activate('en') + self.assertRaises(TypeError, lambda x: inst.decimal + Decimal('0.19')) + self.assertEqual(None, inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(None, inst.decimal_en) + + inst.decimal = Decimal('0.42') + self.assertEqual(Decimal('0.42'), inst.decimal) + self.assertEqual(Decimal('0.23'), inst.decimal_de) + self.assertEqual(Decimal('0.42'), inst.decimal_en) + + def test_translated_models_date_instance(self): + inst = models.OtherFieldsModel() + inst.date = datetime.date(2012, 12, 31) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.date(2012, 12, 31), inst.date) + self.assertEqual(datetime.date(2012, 12, 31), inst.date_de) + self.assertEqual(None, inst.date_en) + + inst.date = datetime.date(1999, 1, 1) + inst.save() + self.assertEqual(datetime.date(1999, 1, 1), inst.date) + self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) + self.assertEqual(None, inst.date_en) + + qs = models.OtherFieldsModel.objects.filter(date='1999-1-1') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].date, datetime.date(1999, 1, 1)) + + trans_real.activate('en') + inst.date = datetime.date(2012, 12, 31) + self.assertEqual(datetime.date(2012, 12, 31), inst.date) + self.assertEqual(datetime.date(1999, 1, 1), inst.date_de) + self.assertEqual(datetime.date(2012, 12, 31), inst.date_en) + + def test_translated_models_datetime_instance(self): + inst = models.OtherFieldsModel() + inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_de) + self.assertEqual(None, inst.datetime_en) + + inst.datetime = datetime.datetime(1999, 1, 1, 23, 42) + inst.save() + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) + self.assertEqual(None, inst.datetime_en) + + qs = models.OtherFieldsModel.objects.filter(datetime='1999-1-1 23:42') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].datetime, datetime.datetime(1999, 1, 1, 23, 42)) + + trans_real.activate('en') + inst.datetime = datetime.datetime(2012, 12, 31, 23, 42) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime) + self.assertEqual(datetime.datetime(1999, 1, 1, 23, 42), inst.datetime_de) + self.assertEqual(datetime.datetime(2012, 12, 31, 23, 42), inst.datetime_en) + + def test_translated_models_time_instance(self): + inst = models.OtherFieldsModel() + inst.time = datetime.time(23, 42, 0) + self.assertEqual('de', get_language()) + self.assertEqual(datetime.time(23, 42, 0), inst.time) + self.assertEqual(datetime.time(23, 42, 0), inst.time_de) + self.assertEqual(None, inst.time_en) + + inst.time = datetime.time(1, 2, 3) + inst.save() + self.assertEqual(datetime.time(1, 2, 3), inst.time) + self.assertEqual(datetime.time(1, 2, 3), inst.time_de) + self.assertEqual(None, inst.time_en) + + qs = models.OtherFieldsModel.objects.filter(time='01:02:03') + self.assertEqual(len(qs), 1) + self.assertEqual(qs[0].time, datetime.time(1, 2, 3)) + + trans_real.activate('en') + inst.time = datetime.time(23, 42, 0) + self.assertEqual(datetime.time(23, 42, 0), inst.time) + self.assertEqual(datetime.time(1, 2, 3), inst.time_de) + self.assertEqual(datetime.time(23, 42, 0), inst.time_en) + + def test_dates_queryset(self): + Model = models.OtherFieldsModel + + Model.objects.create(datetime=datetime.datetime(2015, 9, 2, 0, 0)) + Model.objects.create(datetime=datetime.datetime(2014, 8, 3, 0, 0)) + Model.objects.create(datetime=datetime.datetime(2013, 7, 4, 0, 0)) + + qs = Model.objects.dates('datetime', 'year', 'DESC') + + self.assertEqual(list(qs), [ + datetime.date(2015, 1, 1), + datetime.date(2014, 1, 1), + datetime.date(2013, 1, 1) + ]) + + def test_descriptors(self): + # Descriptor store ints in database and returns string of 'a' of that length + inst = models.DescriptorModel() + # Demonstrate desired behaviour + inst.normal = 2 + self.assertEqual('aa', inst.normal) + inst.normal = 'abc' + self.assertEqual('aaa', inst.normal) + + # Descriptor on translated field works too + self.assertEqual('de', get_language()) + inst.trans = 5 + self.assertEqual('aaaaa', inst.trans) + + inst.save() + db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] + self.assertEqual(3, db_values['normal']) + self.assertEqual(5, db_values['trans_de']) + self.assertEqual(0, db_values['trans_en']) + + # Retrieval from db + inst = models.DescriptorModel.objects.all()[0] + self.assertEqual('aaa', inst.normal) + self.assertEqual('aaaaa', inst.trans) + self.assertEqual('aaaaa', inst.trans_de) + self.assertEqual('', inst.trans_en) + + # Other language + trans_real.activate('en') + self.assertEqual('', inst.trans) + inst.trans = 'q' + self.assertEqual('a', inst.trans) + inst.trans_de = 4 + self.assertEqual('aaaa', inst.trans_de) + inst.save() + db_values = models.DescriptorModel.objects.raw_values('normal', 'trans_en', 'trans_de')[0] + self.assertEqual(3, db_values['normal']) + self.assertEqual(4, db_values['trans_de']) + self.assertEqual(1, db_values['trans_en']) + + +class ModeltranslationTestRule1(ModeltranslationTestBase): + """ + Rule 1: Reading the value from the original field returns the value in + translated to the current language. + """ + def _test_field(self, field_name, value_de, value_en, deactivate=True): + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + params = {field_name_de: value_de, field_name_en: value_en} + + n = models.TestModel.objects.create(**params) + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + self.assertEqual(getattr(n, field_name), value_de) + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + # Now switch to "en" + trans_real.activate("en") + self.assertEqual(get_language(), "en") + # Should now be return the english one (just by switching the language) + self.assertEqual(getattr(n, field_name), value_en) + # But explicit language fields hold their values + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + + n = models.TestModel.objects.create(**params) + n.save() + # Language is set to "en" at this point + self.assertEqual(get_language(), "en") + self.assertEqual(getattr(n, field_name), value_en) + self.assertEqual(getattr(n, field_name_de), value_de) + self.assertEqual(getattr(n, field_name_en), value_en) + trans_real.activate('de') + self.assertEqual(get_language(), 'de') + self.assertEqual(getattr(n, field_name), value_de) + + if deactivate: + trans_real.deactivate() + + def test_rule1(self): + """ + Basic CharField/TextField test. + """ + title1_de = "title de" + title1_en = "title en" + text_de = "Dies ist ein deutscher Satz" + text_en = "This is an english sentence" + + self._test_field(field_name='title', value_de=title1_de, value_en=title1_en) + self._test_field(field_name='text', value_de=text_de, value_en=text_en) + + def test_rule1_url_field(self): + self._test_field(field_name='url', + value_de='http://www.google.de', + value_en='http://www.google.com') + + def test_rule1_email_field(self): + self._test_field(field_name='email', + value_de='django-modeltranslation@googlecode.de', + value_en='django-modeltranslation@googlecode.com') + + +class ModeltranslationTestRule2(ModeltranslationTestBase): + """ + Rule 2: Assigning a value to the original field updates the value + in the associated current language translation field. + """ + def _test_field(self, field_name, value1_de, value1_en, value2, value3, + deactivate=True): + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + params = {field_name_de: value1_de, field_name_en: value1_en} + + self.assertEqual(get_language(), 'de') + n = models.TestModel.objects.create(**params) + self.assertEqual(getattr(n, field_name), value1_de) + self.assertEqual(getattr(n, field_name_de), value1_de) + self.assertEqual(getattr(n, field_name_en), value1_en) + + setattr(n, field_name, value2) + n.save() + self.assertEqual(getattr(n, field_name), value2) + self.assertEqual(getattr(n, field_name_de), value2) + self.assertEqual(getattr(n, field_name_en), value1_en) + + trans_real.activate("en") + self.assertEqual(get_language(), "en") + + setattr(n, field_name, value3) + setattr(n, field_name_de, value1_de) + n.save() + self.assertEqual(getattr(n, field_name), value3) + self.assertEqual(getattr(n, field_name_en), value3) + self.assertEqual(getattr(n, field_name_de), value1_de) + + if deactivate: + trans_real.deactivate() + + def test_rule2(self): + """ + Basic CharField/TextField test. + """ + self._test_field(field_name='title', + value1_de='title de', + value1_en='title en', + value2='Neuer Titel', + value3='new title') + + def test_rule2_url_field(self): + self._test_field(field_name='url', + value1_de='http://www.google.de', + value1_en='http://www.google.com', + value2='http://www.google.at', + value3='http://www.google.co.uk') + + def test_rule2_email_field(self): + self._test_field(field_name='email', + value1_de='django-modeltranslation@googlecode.de', + value1_en='django-modeltranslation@googlecode.com', + value2='django-modeltranslation@googlecode.at', + value3='django-modeltranslation@googlecode.co.uk') + + +class ModeltranslationTestRule3(ModeltranslationTestBase): + """ + Rule 3: If both fields - the original and the current language translation + field - are updated at the same time, the current language translation + field wins. + """ + + def test_rule3(self): + self.assertEqual(get_language(), 'de') + title = 'title de' + + # Normal behaviour + n = models.TestModel(title='foo') + self.assertEqual(n.title, 'foo') + self.assertEqual(n.title_de, 'foo') + self.assertEqual(n.title_en, None) + + # constructor + n = models.TestModel(title_de=title, title='foo') + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # object.create + n = models.TestModel.objects.create(title_de=title, title='foo') + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # Database save/load + n = models.TestModel.objects.get(title_de=title) + self.assertEqual(n.title, title) + self.assertEqual(n.title_de, title) + self.assertEqual(n.title_en, None) + + # This is not subject to Rule 3, because updates are not *at the ame time* + n = models.TestModel() + n.title_de = title + n.title = 'foo' + self.assertEqual(n.title, 'foo') + self.assertEqual(n.title_de, 'foo') + self.assertEqual(n.title_en, None) + + @staticmethod + def _index(list, element): + for i, el in enumerate(list): + if el is element: + return i + raise ValueError + + def test_rule3_internals(self): + # Rule 3 work because translation fields are added to model field list + # later than original field. + original = models.TestModel._meta.get_field('title') + translated_de = models.TestModel._meta.get_field('title_de') + translated_en = models.TestModel._meta.get_field('title_en') + fields = models.TestModel._meta.fields + # Here we cannot use simple list.index, because Field has overloaded __cmp__ + self.assertTrue(self._index(fields, original) < self._index(fields, translated_de)) + self.assertTrue(self._index(fields, original) < self._index(fields, translated_en)) + + +class ModelValidationTest(ModeltranslationTestBase): + """ + Tests if a translation model field validates correctly. + """ + def assertRaisesValidation(self, func): + try: + func() + except ValidationError as e: + return e.message_dict + self.fail('ValidationError not raised.') + + def _test_model_validation(self, field_name, invalid_value, valid_value): + """ + Generic model field validation test. + """ + field_name_de = '%s_de' % field_name + field_name_en = '%s_en' % field_name + # Title need to be passed here - otherwise it would not validate + params = {'title_de': 'title de', 'title_en': 'title en', field_name: invalid_value} + + n = models.TestModel.objects.create(**params) + + # First check the original field + # Expect that the validation object contains an error + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + + # Set translation field to a valid value + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + setattr(n, field_name_de, valid_value) + n.full_clean() + + # All language fields are validated even though original field validation raise no error + setattr(n, field_name_en, invalid_value) + errors = self.assertRaisesValidation(n.full_clean) + self.assertNotIn(field_name, errors) + self.assertIn(field_name_en, errors) + + # When language is changed to en, the original field also doesn't validate + with override('en'): + setattr(n, field_name_en, invalid_value) + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + self.assertIn(field_name_en, errors) + + # Set translation field to an invalid value + setattr(n, field_name_en, valid_value) + setattr(n, field_name_de, invalid_value) + # Expect that the validation object contains an error for url_de + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn(field_name, errors) + self.assertIn(field_name_de, errors) + + def test_model_validation_required(self): + """ + General test for CharField: if required/blank is handled properly. + """ + # Create an object without title (which is required) + n = models.TestModel.objects.create(text='Testtext') + + # First check the original field + # Expect that the validation object contains an error for title + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn('title', errors) + n.save() + + # Check the translation field + # Language is set to 'de' at this point + self.assertEqual(get_language(), 'de') + # Set translation field to a valid title + n.title_de = 'Title' + n.full_clean() + + # Change language to en + # Now validation fails, because current language (en) title is empty + # So requirement validation depends on current language + with override('en'): + errors = self.assertRaisesValidation(n.full_clean) + self.assertIn('title', errors) + + # However, with fallback language (most cases), it validates (because empty title + # falls back to title_de): + with default_fallback(): + n.full_clean() + + # Set translation field to an empty title + n.title_de = None + # Even though the original field isn't optional, translation fields are + # per definition always optional. So we expect that the validation + # object contains no error for title_de. + # However, title still raises error, since it points to empty title_de + errors = self.assertRaisesValidation(n.full_clean) + self.assertNotIn('title_de', errors) + self.assertIn('title', errors) + + def test_model_validation_url_field(self): + self._test_model_validation( + field_name='url', + invalid_value='foo en', + valid_value='http://code.google.com/p/django-modeltranslation/') + + def test_model_validation_email_field(self): + self._test_model_validation( + field_name='email', invalid_value='foo en', + valid_value='django-modeltranslation@googlecode.com') + + +class ModelInheritanceTest(ModeltranslationTestBase): + """Tests for inheritance support in modeltranslation.""" + def test_abstract_inheritance(self): + field_names_b = get_field_names(models.AbstractModelB) + self.assertTrue('titlea' in field_names_b) + self.assertTrue('titlea_de' in field_names_b) + self.assertTrue('titlea_en' in field_names_b) + self.assertTrue('titleb' in field_names_b) + self.assertTrue('titleb_de' in field_names_b) + self.assertTrue('titleb_en' in field_names_b) + self.assertFalse('titled' in field_names_b) + self.assertFalse('titled_de' in field_names_b) + self.assertFalse('titled_en' in field_names_b) + + def test_multitable_inheritance(self): + field_names_a = get_field_names(models.MultitableModelA) + self.assertTrue('titlea' in field_names_a) + self.assertTrue('titlea_de' in field_names_a) + self.assertTrue('titlea_en' in field_names_a) + + field_names_b = get_field_names(models.MultitableModelB) + self.assertTrue('titlea' in field_names_b) + self.assertTrue('titlea_de' in field_names_b) + self.assertTrue('titlea_en' in field_names_b) + self.assertTrue('titleb' in field_names_b) + self.assertTrue('titleb_de' in field_names_b) + self.assertTrue('titleb_en' in field_names_b) + + field_names_c = get_field_names(models.MultitableModelC) + self.assertTrue('titlea' in field_names_c) + self.assertTrue('titlea_de' in field_names_c) + self.assertTrue('titlea_en' in field_names_c) + self.assertTrue('titleb' in field_names_c) + self.assertTrue('titleb_de' in field_names_c) + self.assertTrue('titleb_en' in field_names_c) + self.assertTrue('titlec' in field_names_c) + self.assertTrue('titlec_de' in field_names_c) + self.assertTrue('titlec_en' in field_names_c) + + field_names_d = get_field_names(models.MultitableModelD) + self.assertTrue('titlea' in field_names_d) + self.assertTrue('titlea_de' in field_names_d) + self.assertTrue('titlea_en' in field_names_d) + self.assertTrue('titleb' in field_names_d) + self.assertTrue('titleb_de' in field_names_d) + self.assertTrue('titleb_en' in field_names_d) + self.assertTrue('titled' in field_names_d) + + def test_inheritance(self): + def assertLocalFields(model, local_fields): + # Proper fields are inherited. + opts = translator.translator.get_options_for_model(model) + self.assertEqual(set(opts.local_fields.keys()), set(local_fields)) + # Local translation fields are created on the model. + model_local_fields = [f.name for f in model._meta.local_fields] + for field in local_fields: + for lang in mt_settings.AVAILABLE_LANGUAGES: + translation_field = build_localized_fieldname(field, lang) + self.assertTrue(translation_field in model_local_fields) + + def assertFields(model, fields): + # The given fields are inherited. + opts = translator.translator.get_options_for_model(model) + self.assertEqual(set(opts.fields.keys()), set(fields)) + # Inherited translation fields are available on the model. + model_fields = get_field_names(model) + for field in fields: + for lang in mt_settings.AVAILABLE_LANGUAGES: + translation_field = build_localized_fieldname(field, lang) + self.assertTrue(translation_field in model_fields) + + # Translation fields can be declared on abstract classes. + assertLocalFields(models.Slugged, ('slug',)) + assertLocalFields(models.MetaData, ('keywords',)) + assertLocalFields(models.RichText, ('content',)) + # Local fields are inherited from abstract superclasses. + assertLocalFields(models.Displayable, ('slug', 'keywords',)) + assertLocalFields(models.Page, ('slug', 'keywords', 'title',)) + # But not from concrete superclasses. + assertLocalFields(models.RichTextPage, ('content',)) + + # Fields inherited from concrete models are also available. + assertFields(models.Slugged, ('slug',)) + assertFields(models.Page, ('slug', 'keywords', 'title',)) + assertFields(models.RichTextPage, ('slug', 'keywords', 'title', + 'content',)) + + +class ModelInheritanceFieldAggregationTest(ModeltranslationTestBase): + """ + Tests for inheritance support with field aggregation + in modeltranslation. + """ + def test_field_aggregation(self): + clsb = translation.FieldInheritanceCTranslationOptions + self.assertTrue('titlea' in clsb.fields) + self.assertTrue('titleb' in clsb.fields) + self.assertTrue('titlec' in clsb.fields) + self.assertEqual(3, len(clsb.fields)) + self.assertEqual(tuple, type(clsb.fields)) + + def test_multi_inheritance(self): + clsb = translation.FieldInheritanceETranslationOptions + self.assertTrue('titlea' in clsb.fields) + self.assertTrue('titleb' in clsb.fields) + self.assertTrue('titlec' in clsb.fields) + self.assertTrue('titled' in clsb.fields) + self.assertTrue('titlee' in clsb.fields) + self.assertEqual(5, len(clsb.fields)) # there are no repetitions + + +class UpdateCommandTest(ModeltranslationTestBase): + def test_update_command(self): + # Here it would be convenient to use fixtures - unfortunately, + # fixtures loader doesn't use raw sql but rather creates objects, + # so translation descriptor affects result and we cannot set the + # 'original' field value. + pk1 = models.TestModel.objects.create(title_de='').pk + pk2 = models.TestModel.objects.create(title_de='already').pk + # Due to ``rewrite(False)`` here, original field will be affected. + models.TestModel.objects.all().rewrite(False).update(title='initial') + + # Check raw data using ``values`` + obj1 = models.TestModel.objects.filter(pk=pk1).raw_values()[0] + obj2 = models.TestModel.objects.filter(pk=pk2).raw_values()[0] + self.assertEqual('', obj1['title_de']) + self.assertEqual('initial', obj1['title']) + self.assertEqual('already', obj2['title_de']) + self.assertEqual('initial', obj2['title']) + + call_command('update_translation_fields', verbosity=0) + + obj1 = models.TestModel.objects.get(pk=pk1) + obj2 = models.TestModel.objects.get(pk=pk2) + self.assertEqual('initial', obj1.title_de) + self.assertEqual('already', obj2.title_de) + + +class TranslationAdminTest(ModeltranslationTestBase): + def setUp(self): + super(TranslationAdminTest, self).setUp() + self.test_obj = models.TestModel.objects.create( + title='Testtitle', text='Testtext') + self.site = AdminSite() + + def tearDown(self): + self.test_obj.delete() + super(TranslationAdminTest, self).tearDown() + + def test_default_fields(self): + class TestModelAdmin(admin.TranslationAdmin): + pass + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('title_de', 'title_en', 'text_de', 'text_en', 'url_de', 'url_en', + 'email_de', 'email_en')) + + def test_default_fieldsets(self): + class TestModelAdmin(admin.TranslationAdmin): + pass + + ma = TestModelAdmin(models.TestModel, self.site) + # We expect that the original field is excluded and only the + # translation fields are included in fields + fields = ['title_de', 'title_en', 'text_de', 'text_en', + 'url_de', 'url_en', 'email_de', 'email_en'] + self.assertEqual( + ma.get_fieldsets(request), [(None, {'fields': fields})]) + self.assertEqual( + ma.get_fieldsets(request, self.test_obj), + [(None, {'fields': fields})]) + + def test_field_arguments(self): + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_field_arguments_restricted_on_form(self): + # Using `fields`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `fieldsets`. + class TestModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ['title']})] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + exclude = ['url', 'email'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + + # You can also pass a tuple to `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + exclude = ('url', 'email') + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `fields` and `exclude`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title', 'url'] + exclude = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) + + # Using `fields` and `readonly_fields`. + class TestModelAdmin(admin.TranslationAdmin): + fields = ['title', 'url'] + readonly_fields = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), ('title_de', 'title_en')) + + # Using `readonly_fields`. + # Note: readonly fields are not included in the form. + class TestModelAdmin(admin.TranslationAdmin): + readonly_fields = ['title'] + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('text_de', 'text_en', 'url_de', 'url_en', 'email_de', 'email_en')) + + # Using grouped fields. + # Note: Current implementation flattens the nested fields. + class TestModelAdmin(admin.TranslationAdmin): + fields = (('title', 'url'), 'email',) + + ma = TestModelAdmin(models.TestModel, self.site) + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), + ('title_de', 'title_en', 'url_de', 'url_en', 'email_de', 'email_en')) + + # Using grouped fields in `fieldsets`. + class TestModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ('email', ('title', 'url'))})] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['email_de', 'email_en', 'title_de', 'title_en', 'url_de', 'url_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_field_arguments_restricted_on_custom_form(self): + # Using `fields`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + fields = ['url', 'email'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['url_de', 'url_en', 'email_de', 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Using `exclude`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + exclude = ['url', 'email'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # If both, the custom form an the ModelAdmin define an `exclude` + # option, the ModelAdmin wins. This is Django behaviour. + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + exclude = ['url'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['title_de', 'title_en', 'text_de', 'text_en', 'email_de', + 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Same for `fields`. + class TestModelForm(forms.ModelForm): + class Meta: + model = models.TestModel + fields = ['text', 'title'] + + class TestModelAdmin(admin.TranslationAdmin): + form = TestModelForm + fields = ['email'] + + ma = TestModelAdmin(models.TestModel, self.site) + fields = ['email_de', 'email_en'] + self.assertEqual( + tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + def test_inline_fieldsets(self): + class DataInline(admin.TranslationStackedInline): + model = models.DataModel + fieldsets = [ + ('Test', {'fields': ('data',)}) + ] + + class TestModelAdmin(admin.TranslationAdmin): + exclude = ('title', 'text',) + inlines = [DataInline] + + class DataTranslationOptions(translator.TranslationOptions): + fields = ('data',) + + translator.translator.register(models.DataModel, + DataTranslationOptions) + ma = TestModelAdmin(models.TestModel, self.site) + + fieldsets = [('Test', {'fields': ['data_de', 'data_en']})] + + try: + ma_fieldsets = ma.get_inline_instances( + request)[0].get_fieldsets(request) + except AttributeError: # Django 1.3 fallback + ma_fieldsets = ma.inlines[0]( + models.TestModel, self.site).get_fieldsets(request) + self.assertEqual(ma_fieldsets, fieldsets) + + try: + ma_fieldsets = ma.get_inline_instances( + request)[0].get_fieldsets(request, self.test_obj) + except AttributeError: # Django 1.3 fallback + ma_fieldsets = ma.inlines[0]( + models.TestModel, self.site).get_fieldsets(request, self.test_obj) + self.assertEqual(ma_fieldsets, fieldsets) + + # Remove translation for DataModel + translator.translator.unregister(models.DataModel) + + def test_list_editable(self): + class TestModelAdmin(admin.TranslationAdmin): + list_editable = ['title'] + list_display = ['id', 'title'] + list_display_links = ['id'] + + ma = TestModelAdmin(models.TestModel, self.site) + list_editable = ['title_de', 'title_en'] + list_display = ['id', 'title_de', 'title_en'] + self.assertEqual(tuple(ma.list_editable), tuple(list_editable)) + self.assertEqual(tuple(ma.list_display), tuple(list_display)) + + def test_build_css_class(self): + with reload_override_settings(LANGUAGES=(('de', 'German'), ('en', 'English'), + ('es-ar', 'Argentinian Spanish'),)): + fields = { + 'foo_en': 'foo-en', + 'foo_es_ar': 'foo-es_ar', + 'foo_en_us': 'foo-en_us', + 'foo_bar_de': 'foo_bar-de', + '_foo_en': '_foo-en', + '_foo_es_ar': '_foo-es_ar', + '_foo_bar_de': '_foo_bar-de', + 'foo__en': 'foo_-en', + 'foo__es_ar': 'foo_-es_ar', + 'foo_bar__de': 'foo_bar_-de', + } + for field, css in fields.items(): + self.assertEqual(build_css_class(field), css) + + def test_multitable_inheritance(self): + class MultitableModelAAdmin(admin.TranslationAdmin): + pass + + class MultitableModelBAdmin(admin.TranslationAdmin): + pass + + maa = MultitableModelAAdmin(models.MultitableModelA, self.site) + mab = MultitableModelBAdmin(models.MultitableModelB, self.site) + + self.assertEqual(tuple(maa.get_form(request).base_fields.keys()), + ('titlea_de', 'titlea_en')) + self.assertEqual(tuple(mab.get_form(request).base_fields.keys()), + ('titlea_de', 'titlea_en', 'titleb_de', 'titleb_en')) + + def test_group_fieldsets(self): + # Declared fieldsets take precedence over group_fieldsets + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + fieldsets = [(None, {'fields': ['title']})] + group_fieldsets = True + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + # Now set group_fieldsets only + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + # Only text and title are registered for translation. We expect to get + # three fieldsets. The first which gathers all untranslated field + # (email only) and one for each translation field (text and title). + fieldsets = [ + ('', {'fields': ['email']}), + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('Text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + # Verify that other options are still taken into account + + # Exclude an untranslated field + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + exclude = ('email',) + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fieldsets = [ + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}), + ('Text', {'classes': ('mt-fieldset',), 'fields': ['text_de', 'text_en']}), + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + # Exclude a translation field + class GroupFieldsetsModelAdmin(admin.TranslationAdmin): + group_fieldsets = True + exclude = ('text',) + ma = GroupFieldsetsModelAdmin(models.GroupFieldsetsModel, self.site) + fieldsets = [ + ('', {'fields': ['email']}), + ('Title', {'classes': ('mt-fieldset',), 'fields': ['title_de', 'title_en']}) + ] + self.assertEqual(ma.get_fieldsets(request), fieldsets) + self.assertEqual(ma.get_fieldsets(request, self.test_obj), fieldsets) + + def test_prepopulated_fields(self): + trans_real.activate('de') + self.assertEqual(get_language(), 'de') + + # Non-translated slug based on translated field (using active language) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) + + # Checking multi-field + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname', 'lastname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de', 'lastname_de',)}) + + # Non-translated slug based on non-translated field (no change) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('age',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('age',)}) + + # Translated slug based on non-translated field (all populated on the same value) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug2': ('age',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('age',), 'slug2_de': ('age',)}) + + # Translated slug based on translated field (corresponding) + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug2': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug2_en': ('firstname_en',), + 'slug2_de': ('firstname_de',)}) + + # Check that current active language is used + trans_real.activate('en') + self.assertEqual(get_language(), 'en') + + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_en',)}) + + # Prepopulation language can be overriden by MODELTRANSLATION_PREPOPULATE_LANGUAGE + with reload_override_settings(MODELTRANSLATION_PREPOPULATE_LANGUAGE='de'): + class NameModelAdmin(admin.TranslationAdmin): + prepopulated_fields = {'slug': ('firstname',)} + ma = NameModelAdmin(models.NameModel, self.site) + self.assertEqual(ma.prepopulated_fields, {'slug': ('firstname_de',)}) + + def test_proxymodel_field_argument(self): + class ProxyTestModelAdmin(admin.TranslationAdmin): + fields = ['title'] + + ma = ProxyTestModelAdmin(models.ProxyTestModel, self.site) + fields = ['title_de', 'title_en'] + self.assertEqual(tuple(ma.get_form(request).base_fields.keys()), tuple(fields)) + self.assertEqual( + tuple(ma.get_form(request, self.test_obj).base_fields.keys()), tuple(fields)) + + +class ThirdPartyAppIntegrationTest(ModeltranslationTestBase): + """ + This test case and a test case below have identical tests. The models they test have the same + definition - but in this case the model is not registered for translation and in the other + case it is. + """ + registered = False + + @classmethod + def setUpClass(cls): + # 'model' attribute cannot be assigned to class in its definition, + # because ``models`` module will be reloaded and hence class would use old model classes. + super(ThirdPartyAppIntegrationTest, cls).setUpClass() + cls.model = models.ThirdPartyModel + + def test_form(self): + class CreationForm(forms.ModelForm): + class Meta: + model = self.model + fields = '__all__' + + creation_form = CreationForm({'name': 'abc'}) + inst = creation_form.save() + self.assertEqual('de', get_language()) + self.assertEqual('abc', inst.name) + self.assertEqual(1, self.model.objects.count()) + + +class ThirdPartyAppIntegrationRegisteredTest(ThirdPartyAppIntegrationTest): + registered = True + + @classmethod + def setUpClass(cls): + super(ThirdPartyAppIntegrationRegisteredTest, cls).setUpClass() + cls.model = models.ThirdPartyRegisteredModel + + +class TestManager(ModeltranslationTestBase): + def setUp(self): + # In this test case the default language is en, not de. + super(TestManager, self).setUp() + trans_real.activate('en') + + def test_filter_update(self): + """Test if filtering and updating is language-aware.""" + n = models.ManagerTestModel(title='') + n.title_en = 'en' + n.title_de = 'de' + n.save() + + m = models.ManagerTestModel(title='') + m.title_en = 'title en' + m.title_de = 'de' + m.save() + + self.assertEqual('en', get_language()) + + self.assertEqual(0, models.ManagerTestModel.objects.filter(title='de').count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en').count()) + # Spanning works + self.assertEqual(2, models.ManagerTestModel.objects.filter(title__contains='en').count()) + + with override('de'): + self.assertEqual(2, models.ManagerTestModel.objects.filter(title='de').count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(title='en').count()) + # Spanning works + self.assertEqual(2, models.ManagerTestModel.objects.filter(title__endswith='e').count()) + + # Still possible to use explicit language version + self.assertEqual(1, models.ManagerTestModel.objects.filter(title_en='en').count()) + self.assertEqual(2, models.ManagerTestModel.objects.filter( + title_en__contains='en').count()) + + models.ManagerTestModel.objects.update(title='new') + self.assertEqual(2, models.ManagerTestModel.objects.filter(title='new').count()) + n = models.ManagerTestModel.objects.get(pk=n.pk) + m = models.ManagerTestModel.objects.get(pk=m.pk) + self.assertEqual('en', n.title_en) + self.assertEqual('new', n.title_de) + self.assertEqual('title en', m.title_en) + self.assertEqual('new', m.title_de) + + # Test Python3 "dictionary changed size during iteration" + self.assertEqual(1, models.ManagerTestModel.objects.filter(title='en', + title_en='en').count()) + + def test_q(self): + """Test if Q queries are rewritten.""" + n = models.ManagerTestModel(title='') + n.title_en = 'en' + n.title_de = 'de' + n.save() + + self.assertEqual('en', get_language()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='de') | + Q(pk=42)).count()) + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='en') | + Q(pk=42)).count()) + + with override('de'): + self.assertEqual(1, models.ManagerTestModel.objects.filter(Q(title='de') | + Q(pk=42)).count()) + self.assertEqual(0, models.ManagerTestModel.objects.filter(Q(title='en') | + Q(pk=42)).count()) + + def test_f(self): + """Test if F queries are rewritten.""" + n = models.ManagerTestModel.objects.create(visits_en=1, visits_de=2) + + self.assertEqual('en', get_language()) + models.ManagerTestModel.objects.update(visits=F('visits') + 10) + n = models.ManagerTestModel.objects.all()[0] + self.assertEqual(n.visits_en, 11) + self.assertEqual(n.visits_de, 2) + + with override('de'): + models.ManagerTestModel.objects.update(visits=F('visits') + 20) + n = models.ManagerTestModel.objects.all()[0] + self.assertEqual(n.visits_en, 11) + self.assertEqual(n.visits_de, 22) + + def test_order_by(self): + """Check that field names are rewritten in order_by keys.""" + manager = models.ManagerTestModel.objects + manager.create(title='a') + m = manager.create(title='b') + manager.create(title='c') + with override('de'): + # Make the order of the 'title' column different. + m.title = 'd' + m.save() + titles_asc = tuple(m.title for m in manager.order_by('title')) + titles_desc = tuple(m.title for m in manager.order_by('-title')) + self.assertEqual(titles_asc, ('a', 'b', 'c')) + self.assertEqual(titles_desc, ('c', 'b', 'a')) + + def test_order_by_meta(self): + """Check that meta ordering is rewritten.""" + manager = models.ManagerTestModel.objects + manager.create(title='more_de', visits_en=1, visits_de=2) + manager.create(title='more_en', visits_en=2, visits_de=1) + manager.create(title='most', visits_en=3, visits_de=3) + manager.create(title='least', visits_en=0, visits_de=0) + + # Ordering descending with visits_en + titles_for_en = tuple(m.title_en for m in manager.all()) + with override('de'): + # Ordering descending with visits_de + titles_for_de = tuple(m.title_en for m in manager.all()) + + self.assertEqual(titles_for_en, ('most', 'more_en', 'more_de', 'least')) + self.assertEqual(titles_for_de, ('most', 'more_de', 'more_en', 'least')) + + def assert_fallback(self, method, expected1, *args, **kwargs): + transform = kwargs.pop('transform', lambda x: x) + expected2 = kwargs.pop('expected_de', expected1) + with default_fallback(): + # Fallback is ('de',) + obj = method(*args, **kwargs)[0] + with override('de'): + obj2 = method(*args, **kwargs)[0] + self.assertEqual(transform(obj), expected1) + self.assertEqual(transform(obj2), expected2) + + def test_values_fallback(self): + manager = models.ManagerTestModel.objects + manager.create(title_en='', title_de='de') + self.assertEqual('en', get_language()) + + self.assert_fallback(manager.values, 'de', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, 'de', 'title', flat=True) + self.assert_fallback(manager.values_list, ('de', '', 'de'), 'title', 'title_en', 'title_de') + + # Settings are taken into account - fallback can be disabled + with override_settings(MODELTRANSLATION_ENABLE_FALLBACKS=False): + self.assert_fallback(manager.values, '', 'title', expected_de='de', + transform=lambda x: x['title']) + + # Test fallback values + manager = models.FallbackModel.objects + manager.create() + + self.assert_fallback(manager.values, 'fallback', 'title', transform=lambda x: x['title']) + self.assert_fallback(manager.values_list, ('fallback', 'fallback'), 'title', 'text') + + def test_values(self): + manager = models.ManagerTestModel.objects + id1 = manager.create(title_en='en', title_de='de').pk + + raw_obj = manager.raw_values('title')[0] + obj = manager.values('title')[0] + with override('de'): + raw_obj2 = manager.raw_values('title')[0] + obj2 = manager.values('title')[0] + + # Raw_values returns real database values regardless of current language + self.assertEqual(raw_obj['title'], raw_obj2['title']) + # Values present language-aware data, from the moment of retrieval + self.assertEqual(obj['title'], 'en') + self.assertEqual(obj2['title'], 'de') + + # Values_list behave similarly + self.assertEqual(list(manager.values_list('title', flat=True)), ['en']) + with override('de'): + self.assertEqual(list(manager.values_list('title', flat=True)), ['de']) + + # One can always turn rewrite off + a = list(manager.rewrite(False).values_list('title', flat=True)) + with override('de'): + b = list(manager.rewrite(False).values_list('title', flat=True)) + self.assertEqual(a, b) + + i2 = manager.create(title_en='en2', title_de='de2') + id2 = i2.pk + + # This is somehow repetitive... + self.assertEqual('en', get_language()) + self.assertEqual(list(manager.values('title')), [{'title': 'en'}, {'title': 'en2'}]) + with override('de'): + self.assertEqual(list(manager.values('title')), [{'title': 'de'}, {'title': 'de2'}]) + + # When no fields are passed, list all fields in current language. + self.assertEqual(list(manager.values()), [ + {'id': id1, 'title': 'en', 'visits': 0, 'description': None}, + {'id': id2, 'title': 'en2', 'visits': 0, 'description': None} + ]) + # Similar for values_list + self.assertEqual(list(manager.values_list()), [(id1, 'en', 0, None), (id2, 'en2', 0, None)]) + with override('de'): + self.assertEqual(list(manager.values_list()), + [(id1, 'de', 0, None), (id2, 'de2', 0, None)]) + + # Raw_values + self.assertEqual(list(manager.raw_values()), list(manager.rewrite(False).values())) + i2.delete() + self.assertEqual(list(manager.raw_values()), [ + {'id': id1, 'title': 'en', 'title_en': 'en', 'title_de': 'de', + 'visits': 0, 'visits_en': 0, 'visits_de': 0, + 'description': None, 'description_en': None, 'description_de': None}, + ]) + + # annotation issue (#374) + self.assertEqual(list(manager.values_list('title', flat=True).annotate(Count('title'))), + ['en']) + + def test_values_list_annotation(self): + models.TestModel(title='foo').save() + models.TestModel(title='foo').save() + self.assertEqual( + list(models.TestModel.objects.all().values_list('title').annotate(Count('id'))), + [('foo', 2)] + ) + + def test_custom_manager(self): + """Test if user-defined manager is still working""" + n = models.CustomManagerTestModel(title='') + n.title_en = 'enigma' + n.title_de = 'foo' + n.save() + + m = models.CustomManagerTestModel(title='') + m.title_en = 'enigma' + m.title_de = 'bar' + m.save() + + # Custom method + self.assertEqual('bar', models.CustomManagerTestModel.objects.foo()) + + # Ensure that get_query_set is working - filter objects to those with 'a' in title + self.assertEqual('en', get_language()) + self.assertEqual(2, models.CustomManagerTestModel.objects.count()) + with override('de'): + self.assertEqual(1, models.CustomManagerTestModel.objects.count()) + + def test_custom_manager_custom_method_name(self): + """Test if custom method also returns MultilingualQuerySet""" + from modeltranslation.manager import MultilingualQuerySet + qs = models.CustomManagerTestModel.objects.custom_qs() + self.assertIsInstance(qs, MultilingualQuerySet) + + @skipUnless(MIGRATIONS, 'migrations/auth not available') + def test_3rd_party_custom_manager(self): + from django.contrib.auth.models import Group, GroupManager + from modeltranslation.manager import MultilingualManager + testmodel_fields = get_field_names(Group) + self.assertIn('name', testmodel_fields) + self.assertIn('name_de', testmodel_fields) + self.assertIn('name_en', testmodel_fields) + self.assertIn('name_en', testmodel_fields) + + self.assertIsInstance(Group.objects, MultilingualManager) + self.assertIsInstance(Group.objects, GroupManager) + self.assertIn('get_by_natural_key', dir(Group.objects)) + + def test_multilingual_queryset_pickling(self): + import pickle + from modeltranslation.manager import MultilingualQuerySet + + # typical + models.CustomManagerTestModel.objects.create(title='a') + qs = models.CustomManagerTestModel.objects.all() + serialized = pickle.dumps(qs) + deserialized = pickle.loads(serialized) + self.assertIsInstance(deserialized, MultilingualQuerySet) + self.assertListEqual(list(qs), list(deserialized)) + + # Generated class + models.CustomManager2TestModel.objects.create() + qs = models.CustomManager2TestModel.objects.all() + serialized = pickle.dumps(qs) + deserialized = pickle.loads(serialized) + self.assertIsInstance(deserialized, MultilingualQuerySet) + self.assertIsInstance(deserialized, models.CustomQuerySet) + self.assertListEqual(list(qs), list(deserialized)) + + def test_non_objects_manager(self): + """Test if managers other than ``objects`` are patched too""" + from modeltranslation.manager import MultilingualManager + manager = models.CustomManagerTestModel.another_mgr_name + self.assertTrue(isinstance(manager, MultilingualManager)) + + def test_custom_manager2(self): + """Test if user-defined queryset is still working""" + from modeltranslation.manager import MultilingualManager, MultilingualQuerySet + manager = models.CustomManager2TestModel.objects + self.assertTrue(isinstance(manager, models.CustomManager2)) + self.assertTrue(isinstance(manager, MultilingualManager)) + qs = manager.all() + self.assertTrue(isinstance(qs, models.CustomQuerySet)) + self.assertTrue(isinstance(qs, MultilingualQuerySet)) + + def test_creation(self): + """Test if field are rewritten in create.""" + self.assertEqual('en', get_language()) + n = models.ManagerTestModel.objects.create(title='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + # The same result + n = models.ManagerTestModel.objects.create(title_en='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + # Language suffixed version wins + n = models.ManagerTestModel.objects.create(title='bar', title_en='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + def test_creation_population(self): + """Test if language fields are populated with default value on creation.""" + n = models.ManagerTestModel.objects.populate(True).create(title='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual('foo', n.title_de) + self.assertEqual('foo', n.title) + + # You can specify some language... + n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_de='bar') + self.assertEqual('foo', n.title_en) + self.assertEqual('bar', n.title_de) + self.assertEqual('foo', n.title) + + # ... but remember that still original attribute points to current language + self.assertEqual('en', get_language()) + n = models.ManagerTestModel.objects.populate(True).create(title='foo', title_en='bar') + self.assertEqual('bar', n.title_en) + self.assertEqual('foo', n.title_de) + self.assertEqual('bar', n.title) # points to en + with override('de'): + self.assertEqual('foo', n.title) # points to de + self.assertEqual('en', get_language()) + + # This feature (for backward-compatibility) require populate method... + n = models.ManagerTestModel.objects.create(title='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + # ... or MODELTRANSLATION_AUTO_POPULATE setting + with reload_override_settings(MODELTRANSLATION_AUTO_POPULATE=True): + self.assertEqual(True, mt_settings.AUTO_POPULATE) + n = models.ManagerTestModel.objects.create(title='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual('foo', n.title_de) + self.assertEqual('foo', n.title) + + # populate method has highest priority + n = models.ManagerTestModel.objects.populate(False).create(title='foo') + self.assertEqual('foo', n.title_en) + self.assertEqual(None, n.title_de) + self.assertEqual('foo', n.title) + + # Populate ``default`` fills just the default translation. + # TODO: Having more languages would make these tests more meaningful. + qs = models.ManagerTestModel.objects + m = qs.populate('default').create(title='foo', description='bar') + self.assertEqual('foo', m.title_de) + self.assertEqual('foo', m.title_en) + self.assertEqual('bar', m.description_de) + self.assertEqual('bar', m.description_en) + with override('de'): + m = qs.populate('default').create(title='foo', description='bar') + self.assertEqual('foo', m.title_de) + self.assertEqual(None, m.title_en) + self.assertEqual('bar', m.description_de) + self.assertEqual(None, m.description_en) + + # Populate ``required`` fills just non-nullable default translations. + qs = models.ManagerTestModel.objects + m = qs.populate('required').create(title='foo', description='bar') + self.assertEqual('foo', m.title_de) + self.assertEqual('foo', m.title_en) + self.assertEqual(None, m.description_de) + self.assertEqual('bar', m.description_en) + with override('de'): + m = qs.populate('required').create(title='foo', description='bar') + self.assertEqual('foo', m.title_de) + self.assertEqual(None, m.title_en) + self.assertEqual('bar', m.description_de) + self.assertEqual(None, m.description_en) + + def test_get_or_create_population(self): + """ + Populate may be used with ``get_or_create``. + """ + qs = models.ManagerTestModel.objects + m1, created1 = qs.populate(True).get_or_create(title='aaa') + m2, created2 = qs.populate(True).get_or_create(title='aaa') + self.assertTrue(created1) + self.assertFalse(created2) + self.assertEqual(m1, m2) + self.assertEqual('aaa', m1.title_en) + self.assertEqual('aaa', m1.title_de) + + def test_fixture_population(self): + """ + Test that a fixture with values only for the original fields + does not result in missing default translations for (original) + non-nullable fields. + """ + with auto_populate('required'): + call_command('loaddata', 'fixture.json', verbosity=0, commit=False) + m = models.TestModel.objects.get() + self.assertEqual(m.title_en, 'foo') + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.text_en, 'bar') + self.assertEqual(m.text_de, None) + + def test_fixture_population_via_command(self): + """ + Test that the loaddata command takes new option. + """ + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='required') + m = models.TestModel.objects.get() + self.assertEqual(m.title_en, 'foo') + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.text_en, 'bar') + self.assertEqual(m.text_de, None) + + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate='all') + m = models.TestModel.objects.get() + self.assertEqual(m.title_en, 'foo') + self.assertEqual(m.title_de, 'foo') + self.assertEqual(m.text_en, 'bar') + self.assertEqual(m.text_de, 'bar') + + # Test if option overrides current context + with auto_populate('all'): + call_command('loaddata', 'fixture.json', verbosity=0, commit=False, populate=False) + m = models.TestModel.objects.get() + self.assertEqual(m.title_en, 'foo') + self.assertEqual(m.title_de, None) + self.assertEqual(m.text_en, 'bar') + self.assertEqual(m.text_de, None) + + def assertDeferred(self, use_defer, *fields): + manager = models.TestModel.objects.defer if use_defer else models.TestModel.objects.only + inst1 = manager(*fields)[0] + with override('de'): + inst2 = manager(*fields)[0] + self.assertEqual('title_en', inst1.title) + self.assertEqual('title_en', inst2.title) + with override('de'): + self.assertEqual('title_de', inst1.title) + self.assertEqual('title_de', inst2.title) + + def assertDeferredClass(self, item): + if NEW_DEFERRED_API: + self.assertTrue(len(item.get_deferred_fields()) > 0) + else: + self.assertTrue(item.__class__._deferred) + + def test_deferred(self): + """ + Check if ``only`` and ``defer`` are working. + """ + models.TestModel.objects.create(title_de='title_de', title_en='title_en') + inst = models.TestModel.objects.only('title_en')[0] + if not NEW_DEFERRED_API: + self.assertNotEqual(inst.__class__, models.TestModel) + self.assertTrue(isinstance(inst, models.TestModel)) + self.assertDeferred(False, 'title_en') + + with auto_populate('all'): + self.assertDeferred(False, 'title') + self.assertDeferred(False, 'title_de') + self.assertDeferred(False, 'title_en') + self.assertDeferred(False, 'title_en', 'title_de') + self.assertDeferred(False, 'title', 'title_en') + self.assertDeferred(False, 'title', 'title_de') + # Check if fields are deferred properly with ``only`` + self.assertDeferred(False, 'text') + + # Defer + self.assertDeferred(True, 'title') + self.assertDeferred(True, 'title_de') + self.assertDeferred(True, 'title_en') + self.assertDeferred(True, 'title_en', 'title_de') + self.assertDeferred(True, 'title', 'title_en') + self.assertDeferred(True, 'title', 'title_de') + self.assertDeferred(True, 'text', 'email', 'url') + + def test_deferred_fk(self): + """ + Check if ``select_related`` is rewritten and also + if ``only`` and ``defer`` are working with deferred classes + """ + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(test=test) + + item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] + self.assertDeferredClass(item.test) + self.assertEqual('title_en', item.test.title) + self.assertEqual('title_en', item.test.__class__.objects.only('title')[0].title) + with override('de'): + item = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0] + self.assertDeferredClass(item.test) + self.assertEqual('title_de', item.test.title) + self.assertEqual('title_de', item.test.__class__.objects.only('title')[0].title) + + def test_deferred_spanning(self): + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(test=test) + + item1 = models.ForeignKeyModel.objects.select_related("test").defer("test__text")[0].test + item2 = models.TestModel.objects.defer("text")[0] + self.assertIs(item1.__class__, item2.__class__) + # DeferredAttribute descriptors are present + self.assertIn('text_en', dir(item1.__class__)) + self.assertIn('text_de', dir(item1.__class__)) + + def test_deferred_rule2(self): + models.TestModel.objects.create(title_de='title_de', title_en='title_en') + o = models.TestModel.objects.only('title')[0] + self.assertEqual(o.title, "title_en") + o.title = "bla" + self.assertEqual(o.title, "bla") + + def test_select_related(self): + test = models.TestModel.objects.create(title_de='title_de', title_en='title_en') + with auto_populate('all'): + models.ForeignKeyModel.objects.create(untrans=test) + + fk_qs = models.ForeignKeyModel.objects.all() + self.assertNotIn('_untrans_cache', fk_qs[0].__dict__) + self.assertIn('_untrans_cache', fk_qs.select_related('untrans')[0].__dict__) + self.assertNotIn( + '_untrans_cache', + fk_qs.select_related('untrans').select_related(None)[0].__dict__ + ) + # untrans is nullable so not included when select_related=True + self.assertNotIn('_untrans_cache', fk_qs.select_related()[0].__dict__) + + def test_translation_fields_appending(self): + from modeltranslation.manager import append_lookup_keys, append_lookup_key + self.assertEqual(set(['untrans']), append_lookup_key(models.ForeignKeyModel, 'untrans')) + self.assertEqual(set(['title', 'title_en', 'title_de']), + append_lookup_key(models.ForeignKeyModel, 'title')) + self.assertEqual(set(['test', 'test_en', 'test_de']), + append_lookup_key(models.ForeignKeyModel, 'test')) + self.assertEqual(set(['title__eq', 'title_en__eq', 'title_de__eq']), + append_lookup_key(models.ForeignKeyModel, 'title__eq')) + self.assertEqual(set(['test__smt', 'test_en__smt', 'test_de__smt']), + append_lookup_key(models.ForeignKeyModel, 'test__smt')) + big_set = set(['test__url', 'test__url_en', 'test__url_de', + 'test_en__url', 'test_en__url_en', 'test_en__url_de', + 'test_de__url', 'test_de__url_en', 'test_de__url_de']) + self.assertEqual(big_set, append_lookup_key(models.ForeignKeyModel, 'test__url')) + self.assertEqual(set(['untrans__url', 'untrans__url_en', 'untrans__url_de']), + append_lookup_key(models.ForeignKeyModel, 'untrans__url')) + + self.assertEqual(big_set.union(['title', 'title_en', 'title_de']), + append_lookup_keys(models.ForeignKeyModel, ['test__url', 'title'])) + + def test_constructor_inheritance(self): + inst = models.AbstractModelB() + # Check if fields assigned in constructor hasn't been ignored. + self.assertEqual(inst.titlea, 'title_a') + self.assertEqual(inst.titleb, 'title_b') + + +class TranslationModelFormTest(ModeltranslationTestBase): + def test_fields(self): + class TestModelForm(TranslationModelForm): + class Meta: + model = models.TestModel + fields = '__all__' + + form = TestModelForm() + self.assertEqual(list(form.base_fields), + ['title', 'title_de', 'title_en', 'text', 'text_de', 'text_en', + 'url', 'url_de', 'url_en', 'email', 'email_de', 'email_en']) + self.assertEqual(list(form.fields), ['title', 'text', 'url', 'email']) + + def test_updating_with_empty_value(self): + """ + Can we update the current language translation with an empty value, when + the original field is excluded from the form? + """ + class Form(forms.ModelForm): + class Meta: + model = models.TestModel + exclude = ('text',) + + instance = models.TestModel.objects.create(text_de='something') + form = Form({'text_de': '', 'title': 'a', 'email_de': '', 'email_en': ''}, + instance=instance) + instance = form.save() + self.assertEqual('de', get_language()) + self.assertEqual('', instance.text_de) + + +class ProxyModelTest(ModeltranslationTestBase): + def test_equality(self): + n = models.TestModel.objects.create(title='Title') + m = models.ProxyTestModel.objects.get(title='Title') + self.assertEqual(n.title, m.title) + self.assertEqual(n.title_de, m.title_de) + self.assertEqual(n.title_en, m.title_en) + + +class TestRequired(ModeltranslationTestBase): + def assertRequired(self, field_name): + self.assertFalse(self.opts.get_field(field_name).blank) + + def assertNotRequired(self, field_name): + self.assertTrue(self.opts.get_field(field_name).blank) + + def test_required(self): + self.opts = models.RequiredModel._meta + + # All non required + self.assertNotRequired('non_req') + self.assertNotRequired('non_req_en') + self.assertNotRequired('non_req_de') + + # Original required, but translated fields not - default behaviour + self.assertRequired('req') + self.assertNotRequired('req_en') + self.assertNotRequired('req_de') + + # Set all translated field required + self.assertRequired('req_reg') + self.assertRequired('req_reg_en') + self.assertRequired('req_reg_de') + + # Set some translated field required + self.assertRequired('req_en_reg') + self.assertRequired('req_en_reg_en') + self.assertNotRequired('req_en_reg_de') + + # Test validation + inst = models.RequiredModel() + inst.req = 'abc' + inst.req_reg = 'def' + try: + inst.full_clean() + except ValidationError as e: + error_fields = set(e.message_dict.keys()) + self.assertEqual(set(('req_reg_en', 'req_en_reg', 'req_en_reg_en')), error_fields) + else: + self.fail('ValidationError not raised!') + + +class M2MTest(ModeltranslationTestBase): + def test_m2m(self): + # Create 1 instance of Y, linked to 2 instance of X, with different + # English and German names. + x1 = models.ModelX.objects.create(name_en="foo", name_de="bar") + x2 = models.ModelX.objects.create(name_en="bar", name_de="baz") + y = models.ModelY.objects.create(title='y1') + models.ModelXY.objects.create(model_x=x1, model_y=y) + models.ModelXY.objects.create(model_x=x2, model_y=y) + + with override("en"): + # There's 1 X named "foo" and it's x1 + y_foo = models.ModelY.objects.filter(xs__name="foo") + self.assertEqual(1, y_foo.count()) + + # There's 1 X named "bar" and it's x2 (in English) + y_bar = models.ModelY.objects.filter(xs__name="bar") + self.assertEqual(1, y_bar.count()) + + # But in English, there's no X named "baz" + y_baz = models.ModelY.objects.filter(xs__name="baz") + self.assertEqual(0, y_baz.count()) + + # Again: 1 X named "bar" (but through the M2M field) + x_bar = y.xs.filter(name="bar") + self.assertIn(x2, x_bar) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index c817a899..7f89475e 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -1,13 +1,16 @@ # -*- coding: utf-8 -*- +from django import VERSION +from django.conf import settings from django.utils.translation import ugettext_lazy -from modeltranslation.translator import translator, TranslationOptions +from modeltranslation.translator import translator, register, TranslationOptions from modeltranslation.tests.models import ( TestModel, FallbackModel, FallbackModel2, FileFieldsModel, ForeignKeyModel, OtherFieldsModel, DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page, RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel, CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel, - ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel) + ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel, + RequiredModel, DecoratedModel, ModelX, ModelY) class TestTranslationOptions(TranslationOptions): @@ -21,14 +24,14 @@ class UniqueNullableTranslationOptions(TranslationOptions): translator.register(UniqueNullableModel, UniqueNullableTranslationOptions) -########## Proxy model testing +# ######### Proxy model testing class ProxyTestTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email',) translator.register(ProxyTestModel, ProxyTestTranslationOptions) -########## Fallback values testing +# ######### Fallback values testing class FallbackModelTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email', 'description') @@ -43,14 +46,14 @@ class FallbackModel2TranslationOptions(TranslationOptions): translator.register(FallbackModel2, FallbackModel2TranslationOptions) -########## File fields testing +# ######### File fields testing class FileFieldsModelTranslationOptions(TranslationOptions): fields = ('title', 'file', 'file2', 'image',) translator.register(FileFieldsModel, FileFieldsModelTranslationOptions) -########## Foreign Key / OneToOneField testing +# ######### Foreign Key / OneToOneField testing class ForeignKeyModelTranslationOptions(TranslationOptions): fields = ('title', 'test', 'optional', 'hidden', 'non',) @@ -62,13 +65,11 @@ class OneToOneFieldModelTranslationOptions(TranslationOptions): translator.register(OneToOneFieldModel, OneToOneFieldModelTranslationOptions) -########## Custom fields testing +# ######### Custom fields testing class OtherFieldsModelTranslationOptions(TranslationOptions): -# fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', -# 'ip', 'genericip') fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', - 'ip', 'date', 'datetime', 'time',) + 'ip', 'genericip', 'date', 'datetime', 'time',) translator.register(OtherFieldsModel, OtherFieldsModelTranslationOptions) @@ -77,7 +78,7 @@ class DescriptorModelTranslationOptions(TranslationOptions): translator.register(DescriptorModel, DescriptorModelTranslationOptions) -########## Multitable inheritance testing +# ######### Multitable inheritance testing class MultitableModelATranslationOptions(TranslationOptions): fields = ('titlea',) @@ -94,7 +95,7 @@ class MultitableModelCTranslationOptions(TranslationOptions): translator.register(MultitableModelC, MultitableModelCTranslationOptions) -########## Abstract inheritance testing +# ######### Abstract inheritance testing class AbstractModelATranslationOptions(TranslationOptions): fields = ('titlea',) @@ -106,7 +107,7 @@ class AbstractModelBTranslationOptions(TranslationOptions): translator.register(AbstractModelB, AbstractModelBTranslationOptions) -########## Fields inheritance testing +# ######### Fields inheritance testing class SluggedTranslationOptions(TranslationOptions): fields = ('slug',) @@ -133,7 +134,7 @@ class PageTranslationOptions(TranslationOptions): translator.register(RichTextPage) -########## Manager testing +# ######### Manager testing class ManagerTestModelTranslationOptions(TranslationOptions): fields = ('title', 'visits', 'description') @@ -146,7 +147,7 @@ class CustomManagerTestModelTranslationOptions(TranslationOptions): CustomManagerTestModelTranslationOptions) -########## TranslationOptions field inheritance testing +# ######### TranslationOptions field inheritance testing class FieldInheritanceATranslationOptions(TranslationOptions): fields = ['titlea'] @@ -169,14 +170,14 @@ class FieldInheritanceETranslationOptions(FieldInheritanceCTranslationOptions, fields = ('titlee',) -########## Integration testing +# ######### Integration testing class ThirdPartyTranslationOptions(TranslationOptions): fields = ('name',) translator.register(ThirdPartyRegisteredModel, ThirdPartyTranslationOptions) -########## Admin testing +# ######### Admin testing class GroupFieldsetsTranslationOptions(TranslationOptions): fields = ('title', 'text',) @@ -186,3 +187,43 @@ class GroupFieldsetsTranslationOptions(TranslationOptions): class NameTranslationOptions(TranslationOptions): fields = ('firstname', 'lastname', 'slug2') translator.register(NameModel, NameTranslationOptions) + + +# ######### Required fields testing + +class RequiredTranslationOptions(TranslationOptions): + fields = ('non_req', 'req', 'req_reg', 'req_en_reg') + required_languages = { + 'en': ('req_reg', 'req_en_reg',), + 'default': ('req_reg',), # for all other languages + } +translator.register(RequiredModel, RequiredTranslationOptions) + + +# ######### Decorated registration testing + +@register(DecoratedModel) +class DecoratedTranslationOptions(TranslationOptions): + fields = ('title',) + + +# ######### Complex M2M with abstract classes and custom managers + +class ModelXOptions(TranslationOptions): + fields = ('name',) +translator.register(ModelX, ModelXOptions) + + +class ModelYOptions(TranslationOptions): + fields = ('title',) +translator.register(ModelY, ModelYOptions) + + +# ######### 3-rd party with custom manager + +if VERSION >= (1, 8) and "django.contrib.auth" in settings.INSTALLED_APPS: + from django.contrib.auth.models import Group + + @register(Group) + class GroupTranslationOptions(TranslationOptions): + fields = ('name',) diff --git a/modeltranslation/tests/urls.py b/modeltranslation/tests/urls.py index 653df6d0..2cd14ef3 100644 --- a/modeltranslation/tests/urls.py +++ b/modeltranslation/tests/urls.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- try: from django.conf.urls import include, patterns, url - assert (include, patterns, url) # Workaround for pyflakes issue #13 + # Workaround for pyflakes issue #13 + assert (include, patterns, url) # noqa except ImportError: # Django 1.3 fallback from django.conf.urls.defaults import include, patterns, url # NOQA from django.contrib import admin diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 58f018e4..ebe93ccd 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -1,18 +1,24 @@ # -*- coding: utf-8 -*- +from django import VERSION from django.utils.six import with_metaclass +from django.core.exceptions import ImproperlyConfigured from django.db.models import Manager, ForeignKey, OneToOneField from django.db.models.base import ModelBase from django.db.models.signals import post_init -from django.dispatch import receiver from modeltranslation import settings as mt_settings from modeltranslation.fields import (NONE, create_translation_field, TranslationFieldDescriptor, TranslatedRelationIdDescriptor, LanguageCacheSingleObjectDescriptor) -from modeltranslation.manager import MultilingualManager, rewrite_lookup_key +from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetManager, + rewrite_lookup_key, append_translated) from modeltranslation.utils import build_localized_fieldname, parse_field +NEW_RELATED_API = VERSION >= (1, 9) +NEW_DEFERRED_API = NEW_MANAGER_API = NEW_ABSTRACT_API = VERSION >= (1, 10) + + class AlreadyRegistered(Exception): pass @@ -58,6 +64,7 @@ class TranslationOptions(with_metaclass(FieldsAggregationMetaClass, object)): with translated model. This model may be not translated itself. ``related_fields`` contains names of reverse lookup fields. """ + required_languages = () def __init__(self, model): """ @@ -70,6 +77,28 @@ def __init__(self, model): self.fields = dict((f, set()) for f in self.fields) self.related_fields = [] + def validate(self): + """ + Perform options validation. + """ + # TODO: at the moment only required_languages is validated. + # Maybe check other options as well? + if self.required_languages: + if isinstance(self.required_languages, (tuple, list)): + self._check_languages(self.required_languages) + else: + self._check_languages(self.required_languages.keys(), extra=('default',)) + for fieldnames in self.required_languages.values(): + if any(f not in self.fields for f in fieldnames): + raise ImproperlyConfigured( + 'Fieldname in required_languages which is not in fields option.') + + def _check_languages(self, languages, extra=()): + correct = list(mt_settings.AVAILABLE_LANGUAGES) + list(extra) + if any(l not in correct for l in languages): + raise ImproperlyConfigured( + 'Language in required_languages which is not in AVAILABLE_LANGUAGES.') + def update(self, other): """ Update with options from a superclass. @@ -114,10 +143,16 @@ def add_translation_fields(model, opts): # Construct the name for the localized field localized_field_name = build_localized_fieldname(field_name, l) # Check if the model already has a field by that name + if hasattr(model, localized_field_name): - raise ValueError( - "Error adding translation field. Model '%s' already contains a field named" - "'%s'." % (model._meta.object_name, localized_field_name)) + # Check if are not dealing with abstract field inherited. + for cls in model.__mro__: + if hasattr(cls, '_meta') and cls.__dict__.get(localized_field_name, None): + cls_opts = translator._get_options_for_model(cls) + if not cls._meta.abstract or field_name not in cls_opts.local_fields: + raise ValueError("Error adding translation field. Model '%s' already" + " contains a field named '%s'." % + (model._meta.object_name, localized_field_name)) # This approach implements the translation fields as full valid # django model fields and therefore adds them via add_to_class model.add_to_class(localized_field_name, translation_field) @@ -126,7 +161,19 @@ def add_translation_fields(model, opts): # Rebuild information about parents fields. If there are opts.local_fields, field cache would be # invalidated (by model._meta.add_field() function). Otherwise, we need to do it manually. if len(opts.local_fields) == 0: - model._meta._fill_fields_cache() + try: + model._meta._fill_fields_cache() + except AttributeError: + # Django 1.8 removed _fill_fields_cache + model._meta._expire_cache() + model._meta.get_fields() + + +def has_custom_queryset(manager): + "Check whether manager (or its parents) has declared some custom get_queryset method." + old_diff = getattr(manager, 'get_query_set', None) != getattr(Manager, 'get_query_set', None) + new_diff = getattr(manager, 'get_queryset', None) != getattr(Manager, 'get_queryset', None) + return old_diff or new_diff def add_manager(model): @@ -138,16 +185,68 @@ def add_manager(model): """ if model._meta.abstract: return - for _, attname, cls in model._meta.concrete_managers + model._meta.abstract_managers: - current_manager = getattr(model, attname) - if isinstance(current_manager, MultilingualManager): - continue - if current_manager.__class__ is Manager: - current_manager.__class__ = MultilingualManager + + def patch_manager_class(manager): + if isinstance(manager, MultilingualManager): + return + if manager.__class__ is Manager: + manager.__class__ = MultilingualManager else: - class NewMultilingualManager(MultilingualManager, current_manager.__class__): - pass - current_manager.__class__ = NewMultilingualManager + class NewMultilingualManager(MultilingualManager, manager.__class__, + MultilingualQuerysetManager): + if VERSION < (1, 10): + use_for_related_fields = getattr( + manager.__class__, + "use_for_related_fields", + not has_custom_queryset(manager), + ) + _old_module = manager.__module__ + _old_class = manager.__class__.__name__ + + def deconstruct(self): + return ( + False, # as_manager + '%s.%s' % (self._old_module, self._old_class), # manager_class + None, # qs_class + self._constructor_args[0], # args + self._constructor_args[1], # kwargs + ) + + manager.__class__ = NewMultilingualManager + + if NEW_MANAGER_API: + # Inspired by django.db.models.options.Options.managers (find all + # managers by following the normal Python MRO rules), but keeps the + # original managers instead of making copies. + managers = [] + seen = set() + bases = (b for b in model.mro() if hasattr(b, '_meta')) + for base in bases: + for manager in base._meta.local_managers: + if manager.name in seen: + continue + managers.append(manager) + seen.add(manager.name) + + else: + managers = ((getattr(model, x[1]) for x in + model._meta.concrete_managers + model._meta.abstract_managers)) + + for current_manager in managers: + prev_class = current_manager.__class__ + patch_manager_class(current_manager) + if model._default_manager.__class__ is prev_class: + # Normally model._default_manager is a reference to one of model's managers + # (and would be patched by the way). + # However, in some rare situations (mostly proxy models) + # model._default_manager is not the same instance as one of managers, but it + # share the same class. + model._default_manager.__class__ = current_manager.__class__ + patch_manager_class(model._base_manager) + if VERSION >= (1, 10): + model._meta.base_manager_name = 'objects' + if hasattr(model._meta, "_expire_cache"): + model._meta._expire_cache() def patch_constructor(model): @@ -158,7 +257,7 @@ def patch_constructor(model): def new_init(self, *args, **kwargs): self._mt_init = True - if not self._deferred: + if NEW_DEFERRED_API or not self._deferred: populate_translation_fields(self.__class__, kwargs) for key, val in list(kwargs.items()): new_key = rewrite_lookup_key(model, key) @@ -168,7 +267,6 @@ def new_init(self, *args, **kwargs): model.__init__ = new_init -@receiver(post_init) def delete_mt_init(sender, instance, **kwargs): if hasattr(instance, '_mt_init'): del instance._mt_init @@ -197,6 +295,37 @@ def new_clean_fields(self, exclude=None): model.clean_fields = new_clean_fields +def patch_get_deferred_fields(model): + """ + Django >= 1.8: patch detecting deferred fields. Crucial for only/defer to work. + """ + if not hasattr(model, 'get_deferred_fields'): + return + old_get_deferred_fields = model.get_deferred_fields + + def new_get_deferred_fields(self): + sup = old_get_deferred_fields(self) + if hasattr(self, '_fields_were_deferred'): + sup.update(self._fields_were_deferred) + return sup + model.get_deferred_fields = new_get_deferred_fields + + +def patch_refresh_from_db(model): + """ + Django >= 1.10: patch refreshing deferred fields. Crucial for only/defer to work. + """ + if not hasattr(model, 'refresh_from_db'): + return + old_refresh_from_db = model.refresh_from_db + + def new_refresh_from_db(self, using=None, fields=None): + if fields is not None: + fields = append_translated(self.__class__, fields) + return old_refresh_from_db(self, using, fields) + model.refresh_from_db = new_refresh_from_db + + def patch_metaclass(model): """ Monkey patches original model metaclass to exclude translated fields on deferred subclasses. @@ -215,8 +344,13 @@ class translation_deferred_mcs(old_mcs): def __new__(cls, name, bases, attrs): if attrs.get('_deferred', False): opts = translator.get_options_for_model(model) + were_deferred = set() for field_name in opts.fields.keys(): - attrs.pop(field_name, None) + if attrs.pop(field_name, None): + # Field was deferred. Store this for future reference. + were_deferred.add(field_name) + if len(were_deferred): + attrs['_fields_were_deferred'] = were_deferred return super(translation_deferred_mcs, cls).__new__(cls, name, bases, attrs) # Assign to __metaclass__ wouldn't work, since metaclass search algorithm check for __class__. # http://docs.python.org/2/reference/datamodel.html#__metaclass__ @@ -233,6 +367,9 @@ def delete_cache_fields(model): except AttributeError: pass + if hasattr(model._meta, '_expire_cache'): + model._meta._expire_cache() + def populate_translation_fields(sender, kwargs): """ @@ -336,61 +473,101 @@ def register(self, model_or_iterable, opts_class=None, **options): # Find inherited fields and create options instance for the model. opts = self._get_options_for_model(model, opts_class, **options) - # Mark the object explicitly as registered -- registry caches - # options of all models, registered or not. - opts.registered = True + # If an exception is raised during registration, mark model as not-registered + try: + self._register_single_model(model, opts) + except Exception: + self._registry[model].registered = False + raise - # Add translation fields to the model. + def _register_single_model(self, model, opts): + # Now, when all fields are initialized and inherited, validate configuration. + opts.validate() + + # Mark the object explicitly as registered -- registry caches + # options of all models, registered or not. + opts.registered = True + + # Add translation fields to the model. + if model._meta.proxy: + delete_cache_fields(model) + else: add_translation_fields(model, opts) - # Delete all fields cache for related model (parent and children) - for related_obj in model._meta.get_all_related_objects(): - delete_cache_fields(related_obj.model) + # Delete all fields cache for related model (parent and children) + related = (( + f for f in model._meta.get_fields() + if (f.one_to_many or f.one_to_one) and + f.auto_created + ) if NEW_RELATED_API else model._meta.get_all_related_objects()) - # Set MultilingualManager - add_manager(model) + for related_obj in related: + delete_cache_fields(related_obj.model) - # Patch __init__ to rewrite fields - patch_constructor(model) + # Set MultilingualManager + add_manager(model) - # Patch clean_fields to verify form field clearing - patch_clean_fields(model) + # Patch __init__ to rewrite fields + patch_constructor(model) - # Patch __metaclass__ to allow deferring to work - patch_metaclass(model) + # Connect signal for model + if NEW_DEFERRED_API: + post_init.connect(delete_mt_init, sender=model) + else: + # deferred models have their own classes and the `sender` does not match. + # Connect signal for all models. + post_init.connect(delete_mt_init, dispatch_uid="modeltranslation") - # Substitute original field with descriptor - model_fallback_languages = getattr(opts, 'fallback_languages', None) - model_fallback_values = getattr(opts, 'fallback_values', NONE) - model_fallback_undefined = getattr(opts, 'fallback_undefined', NONE) - for field_name in opts.local_fields.keys(): - field = model._meta.get_field(field_name) - field_fallback_value = parse_field(model_fallback_values, field_name, NONE) - field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE) - descriptor = TranslationFieldDescriptor( - field, - fallback_languages=model_fallback_languages, - fallback_value=field_fallback_value, - fallback_undefined=field_fallback_undefined) - setattr(model, field_name, descriptor) - if isinstance(field, ForeignKey): - # We need to use a special descriptor so that - # _id fields on translated ForeignKeys work - # as expected. - desc = TranslatedRelationIdDescriptor(field_name, model_fallback_languages) - setattr(model, field.get_attname(), desc) - - # Set related field names on other model - if not field.rel.is_hidden(): - other_opts = self._get_options_for_model(field.rel.to) - other_opts.related = True - other_opts.related_fields.append(field.related_query_name()) - add_manager(field.rel.to) # Add manager in case of non-registered model - - if isinstance(field, OneToOneField): - # Fix translated_field caching for SingleRelatedObjectDescriptor - sro_descriptor = getattr(field.rel.to, field.related.get_accessor_name()) - patch_related_object_descriptor_caching(sro_descriptor) + # Patch clean_fields to verify form field clearing + patch_clean_fields(model) + + # Patch __metaclass__ and other methods to allow deferring to work + if not NEW_DEFERRED_API: + patch_metaclass(model) + patch_get_deferred_fields(model) + patch_refresh_from_db(model) + + # Substitute original field with descriptor + model_fallback_languages = getattr(opts, 'fallback_languages', None) + model_fallback_values = getattr(opts, 'fallback_values', NONE) + model_fallback_undefined = getattr(opts, 'fallback_undefined', NONE) + for field_name in opts.local_fields.keys(): + field = model._meta.get_field(field_name) + field_fallback_value = parse_field(model_fallback_values, field_name, NONE) + field_fallback_undefined = parse_field(model_fallback_undefined, field_name, NONE) + descriptor = TranslationFieldDescriptor( + field, + fallback_languages=model_fallback_languages, + fallback_value=field_fallback_value, + fallback_undefined=field_fallback_undefined) + setattr(model, field_name, descriptor) + if isinstance(field, ForeignKey): + # We need to use a special descriptor so that + # _id fields on translated ForeignKeys work + # as expected. + desc = TranslatedRelationIdDescriptor(field_name, model_fallback_languages) + setattr(model, field.get_attname(), desc) + + # Set related field names on other model + if NEW_RELATED_API and not field.remote_field.is_hidden(): + other_opts = self._get_options_for_model(field.remote_field.model) + other_opts.related = True + other_opts.related_fields.append(field.related_query_name()) + # Add manager in case of non-registered model + add_manager(field.remote_field.model) + elif not NEW_RELATED_API and not field.rel.is_hidden(): + other_opts = self._get_options_for_model(field.rel.to) + other_opts.related = True + other_opts.related_fields.append(field.related_query_name()) + add_manager(field.rel.to) # Add manager in case of non-registered model + + if isinstance(field, OneToOneField): + # Fix translated_field caching for SingleRelatedObjectDescriptor + sro_descriptor = ( + getattr(field.remote_field.model, field.remote_field.get_accessor_name()) + if NEW_RELATED_API + else getattr(field.rel.to, field.related.get_accessor_name())) + patch_related_object_descriptor_caching(sro_descriptor) def unregister(self, model_or_iterable): """ @@ -432,6 +609,8 @@ def _get_options_for_model(self, model, opts_class=None, **options): Returns an instance of translation options with translated fields defined for the ``model`` and inherited from superclasses. """ + if not NEW_DEFERRED_API and model._deferred: + model = model._meta.proxy_for_model if model not in self._registry: # Create a new type for backwards compatibility. opts = type("%sTranslationOptions" % model.__name__, @@ -466,3 +645,7 @@ def get_options_for_model(self, model): # This global object represents the singleton translator object translator = Translator() + + +# Re-export the decorator for convenience +from modeltranslation.decorators import register # NOQA re-export diff --git a/modeltranslation/utils.py b/modeltranslation/utils.py old mode 100644 new mode 100755 index 3a1eb64d..81ca7e0f --- a/modeltranslation/utils.py +++ b/modeltranslation/utils.py @@ -4,6 +4,7 @@ from django.utils import six from django.utils.encoding import force_text from django.utils.translation import get_language as _get_language +from django.utils.translation import get_language_info from django.utils.functional import lazy from modeltranslation import settings @@ -15,6 +16,8 @@ def get_language(): settings.LANGUAGES (Django does not seem to guarantee this for us). """ lang = _get_language() + if lang is None: # Django >= 1.8 + return settings.DEFAULT_LANGUAGE if lang not in settings.AVAILABLE_LANGUAGES and '-' in lang: lang = lang.split('-')[0] if lang in settings.AVAILABLE_LANGUAGES: @@ -22,6 +25,14 @@ def get_language(): return settings.DEFAULT_LANGUAGE +def get_language_bidi(lang): + """ + Check if a language is bi-directional. + """ + lang_info = get_language_info(lang) + return lang_info['bidi'] + + def get_translation_fields(field): """ Returns a list of localized fieldnames for a given field. @@ -30,10 +41,16 @@ def get_translation_fields(field): def build_localized_fieldname(field_name, lang): + if lang == 'id': + # The 2-letter Indonesian language code is problematic with the + # current naming scheme as Django foreign keys also add "id" suffix. + lang = 'ind' return str('%s_%s' % (field_name, lang.replace('-', '_'))) def _build_localized_verbose_name(verbose_name, lang): + if lang == 'id': + lang = 'ind' return force_text('%s [%s]') % (force_text(verbose_name), lang) build_localized_verbose_name = lazy(_build_localized_verbose_name, six.text_type) @@ -78,6 +95,10 @@ def build_css_class(localized_fieldname, prefix=''): def unique(seq): """ + Returns a generator yielding unique sequence members in order + + A set by itself will return unique values without any regard for order. + >>> list(unique([1, 2, 3, 2, 2, 4, 1])) [1, 2, 3, 4] """ diff --git a/modeltranslation/widgets.py b/modeltranslation/widgets.py index 6b98186e..942d786f 100644 --- a/modeltranslation/widgets.py +++ b/modeltranslation/widgets.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -from django import VERSION from django.forms.widgets import Media, Widget, CheckboxInput from django.utils.html import conditional_escape from django.utils.safestring import mark_safe @@ -85,15 +84,6 @@ def value_from_datadict(self, data, files, name): return self.empty_value return self.widget.value_from_datadict(data, files, name) - if VERSION < (1, 6): # In Django 1.6 formfields should implement _has_changed - def _has_changed(self, initial, data): - """ - Widget implementation equates ``None``s with empty strings. - """ - if (initial is None and data is not None) or (initial is not None and data is None): - return True - return self.widget._has_changed(initial, data) - def clear_checkbox_name(self, name): """ Given the name of the input, returns the name of the clear checkbox. diff --git a/runtests.py b/runtests.py index a4036bfb..d8f9e805 100755 --- a/runtests.py +++ b/runtests.py @@ -1,7 +1,9 @@ #!/usr/bin/env python import os import sys +import warnings +import django from django.conf import settings from django.core.management import call_command @@ -27,26 +29,28 @@ def runtests(): 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'USER': 'postgres', 'NAME': 'modeltranslation', - 'OPTIONS': { - 'autocommit': True, - } }) # Configure test environment settings.configure( - DATABASES = DATABASES, - INSTALLED_APPS = ( + DATABASES=DATABASES, + INSTALLED_APPS=( + 'django.contrib.contenttypes', + 'django.contrib.auth', 'modeltranslation', ), - ROOT_URLCONF = None, # tests override urlconf, but it still needs to be defined - LANGUAGES = ( + ROOT_URLCONF=None, # tests override urlconf, but it still needs to be defined + LANGUAGES=( ('en', 'English'), ), + MIDDLEWARE_CLASSES=(), ) + django.setup() + warnings.simplefilter('always', DeprecationWarning) failures = call_command( - 'test', 'modeltranslation', interactive=False, failfast=False, - verbosity=2) + 'test', 'modeltranslation', interactive=False, failfast=False, verbosity=2) + sys.exit(bool(failures)) diff --git a/setup.py b/setup.py index 8a994900..45ea859a 100755 --- a/setup.py +++ b/setup.py @@ -25,16 +25,16 @@ 'modeltranslation.management.commands'], package_data={'modeltranslation': ['static/modeltranslation/css/*.css', 'static/modeltranslation/js/*.js']}, - requires=['django(>=1.3)'], + requires=['Django(>=1.8)'], download_url='https://github.com/deschler/django-modeltranslation/archive/%s.tar.gz' % version, classifiers=[ 'Programming Language :: Python', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Operating System :: OS Independent', 'Environment :: Web Environment', 'Intended Audience :: Developers', diff --git a/tox.ini b/tox.ini index 18ee6b13..7bf0847b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,24 @@ +[flake8] +max-line-length = 100 +exclude = .tox,docs/modeltranslation/conf.py + [tox] distribute = False envlist = - py33-1.6.X, - py32-1.6.X, - py27-1.6.X, - py26-1.6.X, - py33-1.5.X, - py32-1.5.X, - py27-1.5.X, - py26-1.5.X, - py27-1.4.X, - py26-1.4.X + py36-1.11.X, + py35-1.11.X, + py34-1.11.X, + py27-1.11.X, + py35-1.10.X, + py34-1.10.X, + py27-1.10.X, + py35-1.9.X, + py34-1.9.X, + py27-1.9.X, + py35-1.8.X, + py34-1.8.X, + py33-1.8.X, + py27-1.8.X, [testenv] downloadcache = {toxworkdir}/_download/ @@ -19,62 +27,86 @@ commands = {envpython} runtests.py -[testenv:py33-1.6.X] -basepython = python3.3 +[testenv:py36-1.11.X] +basepython = python3.6 deps = - Django>=1.6,<1.7 + Django>=1.11,<1.12 Pillow -[testenv:py32-1.6.X] -basepython = python3.2 +[testenv:py35-1.11.X] +basepython = python3.5 deps = - Django>=1.6,<1.7 + Django>=1.11,<1.12 Pillow -[testenv:py27-1.6.X] -basepython = python2.7 +[testenv:py34-1.11.X] +basepython = python3.4 deps = - Django>=1.6,<1.7 + Django>=1.11,<1.12 Pillow -[testenv:py26-1.6.X] -basepython = python2.6 +[testenv:py27-1.11.X] +basepython = python2.7 deps = - Django>=1.6,<1.7 + Django>=1.11,<1.12 Pillow -[testenv:py33-1.5.X] -basepython = python3.3 +[testenv:py35-1.10.X] +basepython = python3.5 deps = - Django>=1.5,<1.6 + Django>=1.10,<1.11 Pillow -[testenv:py32-1.5.X] -basepython = python3.2 +[testenv:py34-1.10.X] +basepython = python3.4 deps = - Django>=1.5,<1.6 + Django>=1.10,<1.11 Pillow -[testenv:py27-1.5.X] +[testenv:py27-1.10.X] basepython = python2.7 deps = - Django>=1.5,<1.6 + Django>=1.10,<1.11 Pillow -[testenv:py26-1.5.X] -basepython = python2.6 +[testenv:py35-1.9.X] +basepython = python3.5 deps = - Django>=1.5,<1.6 + Django>=1.9,<1.10 Pillow -[testenv:py27-1.4.X] +[testenv:py34-1.9.X] +basepython = python3.4 +deps = + Django>=1.9,<1.10 + Pillow + +[testenv:py27-1.9.X] basepython = python2.7 deps = - Django>=1.4,<1.5 + Django>=1.9,<1.10 Pillow -[testenv:py26-1.4.X] -basepython = python2.6 +[testenv:py35-1.8.X] +basepython = python3.5 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py34-1.8.X] +basepython = python3.4 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py33-1.8.X] +basepython = python3.3 +deps = + Django>=1.8,<1.9 + Pillow + +[testenv:py27-1.8.X] +basepython = python2.7 deps = - Django>=1.4,<1.5 + Django>=1.8,<1.9 Pillow diff --git a/travis.py b/travis.py index 6f4c4f2c..a4768f5c 100755 --- a/travis.py +++ b/travis.py @@ -5,5 +5,5 @@ if version.startswith('http'): print(version) else: - next_version = float(version) + 0.1 - print('Django>=%s,<%.1f' % (version, next_version)) + next_version = version[:-1] + '%d' % (int(version[-1]) + 1) + print('Django>=%s,<%s' % (version, next_version))