Compare commits

..

7 Commits

Author SHA1 Message Date
Swen Kooij
a21ff53cb9 Bump version number to 5.4.2 2020-03-10 13:16:03 +02:00
Alexandru Arnăutu
e4bd26ece2
Add support for storing Float values (#80)
* Add LocalizedFloatValue

* Add LocalizedFloatField

* Add tests for float field

* Create LocalizedNumericValue with __int__ and __float__ methods
2020-03-10 09:21:11 +02:00
Swen Kooij
a198440a64 Remove annoying quotes around 'master' in README 2019-10-20 18:20:27 +03:00
Swen Kooij
5945c3f531 Update pip install command for 5.4.1 2019-10-20 18:19:07 +03:00
Swen Kooij
3fd862ce4d Bump version number to 5.4.1 2019-10-20 18:15:05 +03:00
Swen Kooij
dad7f164cd Pin dependency down to django-postgres-extra <2.0 2019-10-20 18:14:12 +03:00
Swen Kooij
6490f3908c Add warning about v6 2019-10-20 18:00:29 +03:00
76 changed files with 1400 additions and 3020 deletions

View File

@ -1,9 +1,31 @@
version: 2 version: 2
jobs: jobs:
test-python35:
docker:
- image: python:3.5-alpine
- image: postgres:11.0
environment:
POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields'
POSTGRES_PASSWORD: 'localizedfields'
steps:
- checkout
- run:
name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run:
name: Install Python packages
command: pip install -r requirements/test.txt
- run:
name: Run tests
command: tox -e 'py35-dj{111,20,21}'
environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
test-python36: test-python36:
docker: docker:
- image: python:3.6-alpine - image: python:3.6-alpine
- image: postgres:12.0 - image: postgres:11.0
environment: environment:
POSTGRES_DB: 'localizedfields' POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields' POSTGRES_USER: 'localizedfields'
@ -11,51 +33,21 @@ jobs:
steps: steps:
- checkout - checkout
- run: - run:
name: Install packages name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run: - run:
name: Install Python packages name: Install Python packages
command: pip install --progress-bar off .[test] command: pip install -r requirements/test.txt
- run: - run:
name: Run tests name: Run tests
command: tox -e 'py36-dj{20,21,22,30,31,32}' command: tox -e 'py36-dj{111,20,21}'
environment: environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields' DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results:
path: reports
test-python37: test-python37:
docker: docker:
- image: python:3.7-alpine - image: python:3.7-alpine
- image: postgres:12.0 - image: postgres:11.0
environment:
POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields'
POSTGRES_PASSWORD: 'localizedfields'
steps:
- checkout
- run:
name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run:
name: Install Python packages
command: pip install --progress-bar off .[test]
- run:
name: Run tests
command: tox -e 'py37-dj{20,21,22,30,31,32}'
environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results:
path: reports
test-python38:
docker:
- image: python:3.8-alpine
- image: postgres:12.0
environment: environment:
POSTGRES_DB: 'localizedfields' POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields' POSTGRES_USER: 'localizedfields'
@ -65,126 +57,36 @@ jobs:
- run: - run:
name: Install packages name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run: - run:
name: Install Python packages name: Install Python packages
command: pip install --progress-bar off .[test] command: pip install -r requirements/test.txt
- run: - run:
name: Run tests name: Run tests
command: tox -e 'py38-dj{20,21,22,30,31,32,40,41,42}' command: tox -e 'py37-dj{111,20,21}'
environment: environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields' DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results: lint:
path: reports
test-python39:
docker: docker:
- image: python:3.9-alpine - image: python:3.5-alpine
- image: postgres:12.0
environment:
POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields'
POSTGRES_PASSWORD: 'localizedfields'
steps: steps:
- checkout - checkout
- run: - run:
name: Install packages name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run: - run:
name: Install Python packages name: Install Python packages
command: pip install --progress-bar off .[test] command: pip install -r requirements/test.txt
- run: - run:
name: Run tests name: Lint code
command: tox -e 'py39-dj{30,31,32,40,41,42}' command: python setup.py lint
environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results:
path: reports
test-python310:
docker:
- image: python:3.10-alpine
- image: postgres:12.0
environment:
POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields'
POSTGRES_PASSWORD: 'localizedfields'
steps:
- checkout
- run:
name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run:
name: Install Python packages
command: pip install --progress-bar off .[test]
- run:
name: Run tests
command: tox -e 'py310-dj{32,40,41,42,50}'
environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results:
path: reports
test-python311:
docker:
- image: python:3.11-alpine
- image: postgres:12.0
environment:
POSTGRES_DB: 'localizedfields'
POSTGRES_USER: 'localizedfields'
POSTGRES_PASSWORD: 'localizedfields'
steps:
- checkout
- run:
name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run:
name: Install Python packages
command: pip install --progress-bar off .[test]
- run:
name: Run tests
command: tox -e 'py311-dj{42,50}'
environment:
DATABASE_URL: 'postgres://localizedfields:localizedfields@localhost:5432/localizedfields'
- store_test_results:
path: reports
analysis:
docker:
- image: python:3.8-alpine
steps:
- checkout
- run:
name: Install packages
command: apk add postgresql-libs gcc musl-dev postgresql-dev git
- run:
name: Install Python packages
command: pip install --progress-bar off .[analysis]
- run:
name: Verify formatting / linting
command: python setup.py verify
workflows: workflows:
version: 2 version: 2
build: build:
jobs: jobs:
- test-python35
- test-python36 - test-python36
- test-python37 - test-python37
- test-python38 - lint
- test-python39
- test-python310
- test-python311
- analysis

5
.gitignore vendored
View File

@ -1,7 +1,6 @@
# Ignore virtual environments # Ignore virtual environments
env/ env/
.env/ .env/
venv/
# Ignore Python byte code cache # Ignore Python byte code cache
*.pyc *.pyc
@ -10,13 +9,11 @@ __pycache__
# Ignore coverage reports # Ignore coverage reports
.coverage .coverage
reports/ htmlcov
# Ignore build results # Ignore build results
*.egg-info/ *.egg-info/
dist/ dist/
build/
pip-wheel-metadata
# Ignore stupid .DS_Store # Ignore stupid .DS_Store
.DS_Store .DS_Store

View File

@ -1,10 +0,0 @@
version: 2
sphinx:
builder: html
configuration: docs/source/conf.py
python:
version: 3.7
install:
- requirements: requirements/docs.txt

View File

@ -1,58 +0,0 @@
| | | |
|--------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| :white_check_mark: | **Tests** | [![CircleCI](https://circleci.com/gh/SectorLabs/django-localized-fields/tree/master.svg?style=svg)](https://circleci.com/gh/SectorLabs/django-localized-fields/tree/master) |
| :memo: | **License** | [![License](https://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org) |
| :package: | **PyPi** | [![PyPi](https://badge.fury.io/py/django-localized-fields.svg)](https://pypi.python.org/pypi/django-localized-fields) |
| <img src="https://cdn.iconscout.com/icon/free/png-256/django-1-282754.png" width="22px" height="22px" align="center" /> | **Django Versions** | 2.0, 2.1, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0 |
| <img src="http://www.iconarchive.com/download/i73027/cornmanthe3rd/plex/Other-python.ico" width="22px" height="22px" align="center" /> | **Python Versions** | 3.6, 3.7, 3.8, 3.9, 3.10, 3.11 |
| :book: | **Documentation** | [Read The Docs](https://django-localized-fields.readthedocs.io) |
| :warning: | **Upgrade** | [Upgrade fom v5.x](https://django-localized-fields.readthedocs.io/en/latest/releases.html#v6-0)
| :checkered_flag: | **Installation** | [Installation Guide](https://django-localized-fields.readthedocs.io/en/latest/installation.html) |
`django-localized-fields` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as `models.HStoreField` since Django 1.10.
---
:warning: **This README is for v6. See the `v5.x` branch for v5.x.**
---
## Working with the code
### Prerequisites
* PostgreSQL 12 or newer.
* Django 2.0 or newer.
* Python 3.6 or newer.
### Getting started
1. Clone the repository:
λ git clone https://github.com/SectorLabs/django-localized-fields.git
2. Create a virtual environment:
λ cd django-localized-fields
λ virtualenv env
λ source env/bin/activate
3. Create a postgres user for use in tests (skip if your default user is a postgres superuser):
λ createuser --superuser localized_fields --pwprompt
λ export DATABASE_URL=postgres://localized_fields:<password>@localhost/localized_fields
Hint: if you're using virtualenvwrapper, you might find it beneficial to put
the ``export`` line in ``$VIRTUAL_ENV/bin/postactivate`` so that it's always
available when using this virtualenv.
4. Install the development/test dependencies:
λ pip install ".[test]" ".[analysis]"
5. Run the tests:
λ tox
7. Auto-format code, sort imports and auto-fix linting errors:
λ python setup.py fix

414
README.rst Normal file
View File

@ -0,0 +1,414 @@
django-localized-fields
=======================
.. image:: https://circleci.com/gh/SectorLabs/django-localized-fields/tree/v5.x.svg?style=svg
:target: https://circleci.com/gh/SectorLabs/django-localized-fields/tree/v5.x
.. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg
:target: https://github.com/SectorLabs/django-localized-fields/blob/master/LICENSE.md
.. image:: https://badge.fury.io/py/django-localized-fields.svg
:target: https://pypi.python.org/pypi/django-localized-fields
``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` since Django 1.10.
This package requires Python 3.5 or newer, Django 1.11 or newer and PostgreSQL 9.6 or newer.
----
**This README is for v5.x. Check out the master branch for v6.x and newer.**
----
Contributors
------------
* `seroy <https://github.com/seroy/>`_
* `umazalakain <https://github.com/umazalakain/>`_
Installation
------------
1. Install the package from PyPi:
.. code-block:: bash
$ pip install django-localized-fields==5.4.1
2. Add ``localized_fields`` and ``django.contrib.postgres`` to your ``INSTALLED_APPS``:
.. code-block:: bash
INSTALLED_APPS = [
...
'django.contrib.postgres',
'localized_fields.apps.LocalizedFieldsConfig'
]
3. Set the database engine to ``psqlextra.backend``:
.. code-block:: python
DATABASES = {
'default': {
...
'ENGINE': 'psqlextra.backend'
}
}
3. Set ``LANGUAGES` and `LANGUAGE_CODE`` in your settings:
.. code-block:: python
LANGUAGE_CODE = 'en' # default language
LANGUAGES = (
('en', 'English'),
('nl', 'Dutch'),
('ro', 'Romanian')
)
4. Apply migrations to enable the HStore extension:
.. code-block:: bash
python manage.py migrate
Usage
-----
Preparation
^^^^^^^^^^^
Declare fields on your model as ``LocalizedField``:
.. code-block:: python
from localized_fields.fields import LocalizedField
class MyModel(models.Model):
title = LocalizedField()
``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings:
.. code-block:: python
LANGUAGE_CODE = 'en' # default language
LANGUAGES = (
('en', 'English'),
('nl', 'Dutch'),
('ro', 'Romanian')
)
All the ``LocalizedField`` you define now will be available in the configured languages.
Basic usage
^^^^^^^^^^^
.. code-block:: python
new = MyModel()
new.title.en = 'english title'
new.title.nl = 'dutch title'
new.title.ro = 'romanian title'
new.save()
By changing the active language you can control which language is presented:
.. code-block:: python
from django.utils import translation
translation.activate('nl')
print(new.title) # prints 'dutch title'
translation.activate('en')
print(new.title) # prints 'english title'
Or get it in a specific language:
.. code-block:: python
print(new.title.get('en')) # prints 'english title'
print(new.title.get('ro')) # prints 'romanian title'
print(new.title.get()) # whatever language is the primary one
print(new.title.get('ar', 'haha')) # prints 'haha' if there is no value in arabic
You can also explicitly set a value in a certain language:
.. code-block:: python
new.title.set('en', 'other english title')
new.title.set('nl', 'other dutch title')
new.title.ro = 'other romanian title'
Constraints
^^^^^^^^^^^
**Required/Optional**
Constraints are enforced on a database level.
* Optional filling
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=True, null=True, required=False)
* Make translation required for any language
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=False)
* Make translation required for specific languages
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=['en', 'ro'])
* Make translation required for all languages
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=True)
* By default the primary language **required** and the others optional:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField()
**Uniqueness**
By default the values stored in a ``LocalizedField`` are *not unique*. You can enforce uniqueness for certain languages. This uniqueness constraint is enforced on a database level using a ``UNIQUE INDEX``.
* Enforce uniqueness for one or more languages:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(uniqueness=['en', 'ro'])
* Enforce uniqueness for **all** languages:
.. code-block:: python
from localized_fields.util import get_language_codes
class MyModel(models.Model):
title = LocalizedField(uniqueness=get_language_codes())
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(uniqueness=[('en', 'ro')])
* Enforce uniqueness for **all** languages **together**:
.. code-block:: python
from localized_fields.util import get_language_codes
class MyModel(models.Model):
title = LocalizedField(uniqueness=[(*get_language_codes())])
Other fields
^^^^^^^^^^^^
Besides ``LocalizedField``, there's also:
* ``LocalizedUniqueSlugField``
Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces
uniqueness of slugs on a database level. Usage is the exact same:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField
class MyModel(LocalizedModel):
title = LocalizedField()
slug = LocalizedUniqueSlugField(populate_from='title')
``populate_from`` can be:
- The name of a field.
.. code-block:: python
slug = LocalizedUniqueSlugField(populate_from='name', include_time=True)
- A callable.
.. code-block:: python
def generate_slug(instance):
return instance.title
slug = LocalizedUniqueSlugField(populate_from=generate_slug, include_time=True)
- A tuple of names of fields.
.. code-block:: python
slug = LocalizedUniqueSlugField(populate_from=('name', 'beer') include_time=True)
By setting the option ``include_time=True``
.. code-block:: python
slug = LocalizedUniqueSlugField(populate_from='title', include_time=True)
You can instruct the field to include a part of the current time into
the resulting slug. This is useful if you're running into a lot of collisions.
* ``LocalizedBleachField``
Automatically bleaches the content of the field.
* django-bleach
Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedField, LocalizedBleachField
class MyModel(models.Model):
title = LocalizedField()
description = LocalizedBleachField()
* ``LocalizedIntegerField``
This is an experimental field type introduced in version 5.0 and is subject to change. It also has
some pretty major downsides due to the fact that values are stored as strings and are converted
back and forth.
Allows storing integers in multiple languages. This works exactly like ``LocalizedField`` except that
all values must be integers. Do note that values are stored as strings in your database because
the backing field type is ``hstore``, which only allows storing strings. The ``LocalizedIntegerField``
takes care of ensuring that all values are integers and converts the stored strings back to integers
when retrieving them from the database. Do not expect to be able to do queries such as:
.. code-block:: python
MyModel.objects.filter(score__en__gt=1)
* ``LocalizedCharField`` and ``LocalizedTextField``
These fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL.
``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render.
Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedCharField, LocalizedTextField
class MyModel(models.Model):
title = LocalizedCharField()
description = LocalizedTextField()
* ``LocalizedFileField``
A file-upload field
Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable).
Example usage:
.. code-block:: python
from localized_fields.fields import LocalizedFileField
def my_directory_path(instance, filename, lang):
# file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename>
return '{0}/{0}_{1}'.format(lang, instance.id, filename)
class MyModel(models.Model):
file1 = LocalizedFileField(upload_to='uploads/{lang}/')
file2 = LocalizedFileField(upload_to=my_directory_path)
In template you can access to file attributes:
.. code-block:: django
{# For current active language: #}
{{ model.file.url }} {# output file url #}
{{ model.file.name }} {# output file name #}
{# Or get it in a specific language: #}
{{ model.file.ro.url }} {# output file url for romanian language #}
{{ model.file.ro.name }} {# output file name for romanian language #}
To get access to file instance for current active language use ``localized`` method:
.. code-block:: python
model.file.localized()
Experimental feature
^^^^^^^^^^^^^^^^^^^^
Enables the following experimental features:
* ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value.
* ``LocalizedField`` lookups will lookup by currently active language instead of HStoreField
.. code-block:: python
LOCALIZED_FIELDS_EXPERIMENTAL = True
Django Admin Integration
^^^^^^^^^^^^^^^^^^^^^^^^
To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminMixin``:
.. code-block:: python
from django.contrib import admin
from myapp.models import MyLocalizedModel
from localized_fields.admin import LocalizedFieldsAdminMixin
class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin):
"""Any admin options you need go here"""
admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin)
.. image:: ./images/admin-widget.png
:alt: The appearance of admin widget
Frequently asked questions (FAQ)
--------------------------------
1. Does this package work with Python 2?
No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python.
2. With what Django versions does this package work?
Only Django 1.11 or newer is supported, this includes Django 2.X. This is because we rely on Django's ``HStoreField`` and template-based widget rendering.
3. Does this package come with support for Django Admin?
Yes. Our custom fields come with a special form that will automatically be used in Django Admin if the field is of ``LocalizedField``.
4. Why should I pick this over any of the other translation packages out there?
You should pick whatever you feel comfortable with. This package stores translations in your database without having to have translation tables. It however only works on PostgreSQL.
5. I am using PostgreSQL <9.6, can I use this?
No. The ``hstore`` data type was introduced in PostgreSQL 9.6.
6. I am using this package. Can I give you some beer?
Yes! If you're ever in the area of Cluj-Napoca, Romania, swing by :)

1
docs/.gitignore vendored
View File

@ -1 +0,0 @@
build/

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -1,10 +0,0 @@
import sphinx_rtd_theme
project = 'django-localized-fields'
copyright = '2019, Sector Labs'
author = 'Sector Labs'
extensions = ["sphinx_rtd_theme"]
templates_path = ['_templates']
exclude_patterns = []
html_theme = "sphinx_rtd_theme"
html_static_path = ['_static']

View File

@ -1,100 +0,0 @@
.. _unique_together: https://docs.djangoproject.com/en/2.2/ref/models/options/#unique-together
Constraints
===========
All constraints are enforced by PostgreSQL. Constraints can be applied to all fields documented in :ref:`Fields <fields>`.
.. warning::
Don't forget to run ``python manage.py makemigrations`` after modifying constraints.
Required/optional
-----------------
* Default language required and all other languages optional:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField()
* All languages are optional and the field itself can be ``None``:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=True, null=True, required=False)
* All languages are optional but the field itself cannot be ``None``:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=False)
* Make specific languages required:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=['en', 'ro'])
* Make all languages required:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(blank=False, null=False, required=True)
Uniqueness
----------
.. note::
Uniqueness is enforced by PostgreSQL by creating unique indexes on hstore keys. Keep this in mind when setting up unique constraints. If you already have a unique constraint in place, you do not have to add an additional index as uniqueness is enforced by creating an index.
* Enforce uniqueness for one or more languages:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(uniqueness=['en', 'ro'])
* Enforce uniqueness for all languages:
.. code-block:: python
from localized_fields.util import get_language_codes
class MyModel(models.Model):
title = LocalizedField(uniqueness=get_language_codes())
* Enforce uniqueness for one or more languages together:
.. code-block:: python
class MyModel(models.Model):
title = LocalizedField(uniqueness=[('en', 'ro')])
This is similar to Django's `unique_together`_.
* Enforce uniqueness for all languages together:
.. code-block:: python
from localized_fields.util import get_language_codes
class MyModel(models.Model):
title = LocalizedField(uniqueness=[(*get_language_codes())])

View File

@ -1,19 +0,0 @@
Django Admin
------------
To display ``LocalizedFields`` as a tab bar in Django Admin; inherit your admin model class from ``LocalizedFieldsAdminMixin``:
.. code-block:: python
from django.contrib import admin
from myapp.models import MyLocalizedModel
from localized_fields.admin import LocalizedFieldsAdminMixin
class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin):
"""Any admin options you need go here"""
admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin)
.. image:: _static/django_admin_widget.png

View File

@ -1,99 +0,0 @@
.. _django.db.models.CharField: https://docs.djangoproject.com/en/2.2/ref/models/fields/#charfield
.. _django.db.models.TextField: https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.TextField
.. _django.db.models.IntegerField: https://docs.djangoproject.com/en/2.2/ref/models/fields/#integerfield
.. _django.db.models.FileField: https://docs.djangoproject.com/en/2.2/ref/models/fields/#filefield
.. _fields:
Localized fields
================
LocalizedField
--------------
Base localized fields. Stores content as strings of arbitrary lengths.
.. code-block:: python
from localized_fields.fields import LocalizedField
LocalizedCharField
------------------
Localized version of `django.db.models.CharField`_.
Use this for single-line text content. Uses ``<input type="text" />`` when rendered as a widget.
.. code-block:: python
from localized_fields.fields import LocalizedCharField
Follows the same convention as `django.db.models.CharField`_ to store empty strings for "no data" and not NULL.
LocalizedTextField
------------------
Localized version of `django.db.models.TextField`_.
Use this for multi-line text content.
.. code-block:: python
from localized_fields.fields import LocalizedTextField
Follows the same convention as `django.db.models.TextField`_ to store empty strings for "no data" and not NULL.
LocalizedFileField
------------------
Localized version of `django.db.models.FileField`_.
.. code-block:: python
from localized_fields.fields import LocalizedFileField
def my_directory_path(instance, filename, lang):
# file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename>
return '{0}/{0}_{1}'.format(lang, instance.id, filename)
class MyModel(models.Model):
file1 = LocalizedFileField(upload_to='uploads/{lang}/')
file2 = LocalizedFileField(upload_to=my_directory_path)
The ``upload_to`` supports the ``{lang}}`` placeholder for string formatting or as function argument (in case if upload_to is a callable).
In a template, you can access the files for different languages:
.. code-block:: html
{# For current active language: #}
{{ model.file.url }} {# output file url #}
{{ model.file.name }} {# output file name #}
{# Or get it in a specific language: #}
{{ model.file.ro.url }} {# output file url for romanian language #}
{{ model.file.ro.name }} {# output file name for romanian language #}
To get the file instance for the current language:
.. code-block:: python
model.file.localized()
LocalizedIntegerField
---------------------
Localized version of `django.db.models.IntegerField`_.
.. code-block:: python
from localized_fields.fields import LocalizedIntegerField
Although the underlying PostgreSQL data type for ``LocalizedField`` is hstore (which only stores strings). ``LocalizedIntegerField`` takes care of making sure that input values are integers and casts the values back to integers when querying them.

View File

@ -1,44 +0,0 @@
Filtering localized content
===========================
.. note::
All examples below assume a model declared like this:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
title = LocalizedField()
Active language
----------------
.. code-block:: python
from django.utils import translation
# filter in english
translation.activate("en")
MyModel.objects.filter(title="test")
# filter in dutch
translation.activate("nl")
MyModel.objects.filter(title="test")
Specific language
-----------------
.. code-block:: python
MyModel.objects.filter(title__en="test")
MyModel.objects.filter(title__nl="test")
# do it dynamically, where the language code is a var
lang_code = "nl"
MyModel.objects.filter(**{"title_%s" % lang_code: "test"})

View File

@ -1,22 +0,0 @@
Welcome
=======
``django-localized-fields`` is a Django library that provides fields to store localized content (content in various languages) in a PostgreSQL database. It does this by utilizing the PostgreSQL ``hstore`` type, which is available in Django as ``HStoreField`` since Django 1.10.
This package requires Python 3.6 or newer, Django 2.0 or newer and PostgreSQL 10 or newer.
.. toctree::
:maxdepth: 2
:caption: Overview
installation
quick_start
constraints
fields
saving
querying
filtering
localized_value
django_admin
settings
releases

View File

@ -1,73 +0,0 @@
.. _installation:
Installation
============
1. Install the package from PyPi:
.. code-block:: bash
$ pip install django-localized-fields
2. Add ``django.contrib.postgres``, ``psqlextra`` and ``localized_fields`` to your ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
...
"django.contrib.postgres",
"psqlextra",
"localized_fields",
]
3. Set the database engine to ``psqlextra.backend``:
.. code-block:: python
DATABASES = {
"default": {
...
"ENGINE": "psqlextra.backend",
],
}
.. note::
Already using a custom back-end? Set ``POSTGRES_EXTRA_DB_BACKEND_BASE`` to your custom back-end. See django-postgres-extra's documentation for more details: `Using a custom database back-end <https://django-postgres-extra.readthedocs.io/en/latest/db-engine/#using-a-custom-database-back-end>`_.
4. Set ``LANGUAGES`` and ``LANGUAGE_CODE``:
.. code-block:: python
LANGUAGE_CODE = "en" # default language
LANGUAGES = (
("en", "English"), # default language
("ar", "Arabic"),
("ro", "Romanian"),
)
.. warning::
Make sure that the language specified in ``LANGUAGE_CODE`` is the first language in the ``LANGUAGES`` list. Django and many third party packages assume that the default language is the first one in the list.
5. Apply migrations to enable the HStore extension:
.. code-block:: bash
$ python manage.py migrate
.. note::
Migrations might fail to be applied if the PostgreSQL user applying the migration is not a super user. Enabling/creating extensions requires superuser permission. Not a superuser? Ask your database administrator to create the ``hstore`` extension on your PostgreSQL server manually using the following statement:
.. code-block:: sql
CREATE EXTENSION IF NOT EXISTS hstore;
Then, fake apply the migration to tell Django that the migration was applied already:
.. code-block:: bash
python manage.py migrate localized_fields --fake

View File

@ -1,79 +0,0 @@
Working with localized values
=============================
.. note::
All examples below assume a model declared like this:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
title = LocalizedField()
Localized content is represented by ``localized_fields.value.LocalizedValue``. Which is essentially a dictionary where the key is the language and the value the content in the respective language.
.. code-block:: python
from localized_fields.value import LocalizedValue
obj = MyModel.objects.first()
assert isistance(obj.title, LocalizedValue) # True
With fallback
-------------
.. seealso::
Configure :ref:`LOCALIZED_FIELDS_FALLBACKS <LOCALIZED_FIELDS_FALLBACKS>` to control the fallback behaviour.
Active language
***************
.. code-block:: python
# gets content in Arabic, falls back to next language
# if not availble
translation.activate('ar')
obj.title.translate()
# alternative: cast to string
title_ar = str(obj.title)
Specific language
*****************
.. code-block:: python
# gets content in Arabic, falls back to next language
# if not availble
obj.title.translate('ar')
Without fallback
----------------
Specific language
*****************
.. code-block:: python
# gets content in Dutch, None if not available
# no fallback to secondary languages here!
obj.title.nl
Specific language dynamically
*****************************
.. code-block:: python
# gets content in Dutch, None if not available
# no fallback to secondary languages here!
obj.title.get('nl')

View File

@ -1,59 +0,0 @@
Querying localized content
==========================
.. note::
All examples below assume a model declared like this:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
title = LocalizedField()
Active language
---------------
Only need a value in a specific language? Use the ``LocalizedRef`` expression to query a value in the currently active language:
.. code-block:: python
from localized_fields.expressions import LocalizedRef
MyModel.objects.create(title=dict(en="Hello", nl="Hallo"))
translation.activate("nl")
english_title = (
MyModel
.objects
.annotate(title=LocalizedRef("title"))
.values_list("title", flat=True)
.first()
)
print(english_title) # prints "Hallo"
Specific language
-----------------
.. code-block:: python
from localized_fields.expressions import LocalizedRef
result = (
MyModel
.objects
.values(
'title__en',
'title__nl',
)
.first()
)
print(result['title__en'])
print(result['title__nl'])

View File

@ -1,140 +0,0 @@
.. _django.utils.translation.override: https://docs.djangoproject.com/en/2.2/ref/utils/#django.utils.translation.override
.. _django.db.models.TextField: https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.TextField
.. _LANGUAGES: https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-LANGUAGE_CODE
.. _LANGUAGE_CODE: https://docs.djangoproject.com/en/2.2/ref/settings/#languages
Quick start
===========
.. warning::
This assumes you have followed the :ref:`Installation guide <installation>`.
``django-localized-fields`` provides various model field types to store content in multiple languages. The most basic of them all is ``LocalizedField`` which stores text of arbitrary length (like `django.db.models.TextField`_).
Declaring a model
-----------------
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
title = LocalizedField()
This creates a model with one localized field. Inside the ``LocalizedField``, strings can be stored in multiple languages. There are more fields like this for different data types (integers, images etc). ``LocalizedField`` is the most basic of them all.
Saving localized content
------------------------
You can now save text in ``MyModel.title`` in all languages you defined in the `LANGUAGES`_ setting. A short example:
.. code-block:: python
newobj = MyModel()
newobj.title.en = "Hello"
newobj.title.ar = "مرحبا"
newobj.title.nl = "Hallo"
newobj.save()
There are various other ways of saving localized content. For example, all fields can be set at once by assigning a ``dict``:
.. code-block:: python
newobj = MyModel()
newobj.title = dict(en="Hello", ar="مرحبا", nl="Hallo")
newobj.save()
This also works when using the ``create`` function:
.. code-block:: python
newobj = MyModel.objects.create(title=dict(en="Hello", ar="مرحبا", nl="Hallo"))
Need to set the content dynamically? Use the ``set`` function:
.. code-block:: python
newobj = MyModel()
newobj.title.set("en", "Hello")
.. note::
Localized field values (``localized_fields.value.LocalizedValue``) act like dictionaries. In fact, ``LocalizedValue`` extends ``dict``. Anything that works on a ``dict`` works on ``LocalizedValue``.
Retrieving localized content
----------------------------
When querying, the currently active language is taken into account. If there is no active language set, the default language is returned (set by the `LANGUAGE_CODE`_ setting).
.. code-block:: python
from django.utils import translation
obj = MyModel.objects.first()
print(obj.title) # prints "Hello"
translation.activate("ar")
print(obj.title) # prints "مرحبا"
str(obj.title) # same as printing, forces translation to active language
translation.activate("nl")
print(obj.title) # prints "Hallo"
.. note::
Use `django.utils.translation.override`_ to change the language for just a block of code rather than setting the language globally:
.. code-block:: python
from django.utils import translation
with translation.override("nl"):
print(obj.title) # prints "Hallo"
Fallback
********
If there is no content for the currently active language, a fallback kicks in where the content will be returned in the next language. The fallback order is controlled by the order set in the `LANGUAGES`_ setting.
.. code-block:: python
obj = MyModel.objects.create(dict(en="Hallo", ar="مرحبا"))
translation.activate("nl")
print(obj.title) # prints "مرحبا" because there"s no content in NL
.. seealso::
Use the :ref:`LOCALIZED_FIELDS_FALLBACKS <LOCALIZED_FIELDS_FALLBACKS>` setting to control the fallback behaviour.
Cast to str
***********
Want to get the value in the currently active language without casting to ``str``? (For null-able fields for example). Use the ``.translate()`` function:
.. code-block:: python
obj = MyModel.objects.create(dict(en="Hallo", ar="مرحبا"))
str(obj.title) == obj.title.translate() # True
.. note::
``str(..)`` is guarenteed to return a string. If the value is ``None``, ``str(..)`` returns an empty string. ``translate()`` would return ``None``. This is because Python forces the ``__str__`` function to return a string.
.. code-block:: python
obj = MyModel.objects.create(dict(en="Hallo"))
translation.activate('nl')
str(obj.title) # ""
obj.title.translate() # None

View File

@ -1,22 +0,0 @@
Releases
========
v6.0
----
Breaking changes
****************
* Removes support for Python 3.6 and earlier.
* Removes support for PostgreSQL 9.6 and earlier.
* Sets ``LOCALIZED_FIELDS_EXPERIMENTAL`` to ``True`` by default.
Bug fixes
*********
* Fixes a bug where ``LocalizedIntegerField`` could not be used in ``order_by``.
Other
*****
* ``LocalizedValue.translate()`` can now takes an optional ``language`` parameter.

View File

@ -1,82 +0,0 @@
Saving localized content
========================
.. note::
All examples below assume a model declared like this:
.. code-block:: python
from localized_fields.models import LocalizedModel
from localized_fields.fields import LocalizedField
class MyModel(LocalizedModel):
title = LocalizedField()
Individual assignment
*********************
.. code-block:: python
obj = MyModel()
obj.title.en = 'Hello'
obj.title.nl = 'Hallo'
obj.save()
Individual dynamic assignment
*****************************
.. code-block:: python
obj = MyModel()
obj.title.set('en', 'Hello')
obj.title.set('nl', 'Hallo')
obj.save()
Multiple assignment
*******************
.. code-block:: python
obj = MyModel()
obj.title = dict(en='Hello', nl='Hallo')
obj.save()
obj = MyModel(title=dict(en='Hello', nl='Hallo'))
obj.save()
obj = MyModel.objects.create(title=dict(en='Hello', nl='Hallo'))
Default language assignment
***************************
.. code-block:: python
obj = MyModel()
obj.title = 'Hello' # assumes value is in default language
obj.save()
obj = MyModel(title='Hello') # assumes value is in default language
obj.save()
obj = MyModel.objects.create(title='title') # assumes value is in default language
Array assignment
****************
.. code-block:: python
obj = MyModel()
obj.title = ['Hello', 'Hallo'] # order according to LANGUAGES
obj.save()
obj = MyModel(title=['Hello', 'Hallo']) # order according to LANGUAGES
obj.save()
obj = MyModel.objects.create(title=['Hello', 'Hallo']) # order according to LANGUAGES

View File

@ -1,39 +0,0 @@
.. _LANGUAGES: https://docs.djangoproject.com/en/2.2/ref/settings/#std:setting-LANGUAGE_CODE
.. _LANGUAGE_CODE: https://docs.djangoproject.com/en/2.2/ref/settings/#languages
Settings
========
.. LOCALIZED_FIELDS_EXPERIMENTAL:
* ``LOCALIZED_FIELDS_EXPERIMENTAL``
.. note::
Disabled in v5.x and earlier. Enabled by default since v6.0.
When enabled:
* ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value.
* ``LocalizedField`` lookups will by the currently active language instead of an exact match by dict.
.. _LOCALIZED_FIELDS_FALLBACKS:
* ``LOCALIZED_FIELDS_FALLBACKS``
List of language codes which define the order in which fallbacks should happen. If a value is not available in a specific language, we'll try to pick the value in the next language in the list.
.. warning::
If this setting is not configured, the default behaviour is to fall back to the value in the **default language**. It is recommended to configure this setting to get predictible fallback behaviour that suits your use case.
Use the same language codes as you used for configuring the `LANGUAGES`_ and `LANGUAGE_CODE`_ setting.
.. code-block:: python
LOCALIZED_FIELDS_FALLBACKS = {
"en": ["nl", "ar"], # if trying to get EN, but not available, try NL and then AR
"nl": ["en", "ar"], # if trying to get NL, but not available, try EN and then AR
"ar": ["en", "nl"], # if trying to get AR, but not available, try EN and then NL
}

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -1,4 +1 @@
import django default_app_config = 'localized_fields.apps.LocalizedFieldsConfig'
if django.VERSION < (3, 2):
default_app_config = "localized_fields.apps.LocalizedFieldsConfig"

View File

@ -1,18 +1,13 @@
from . import widgets from . import widgets
from .fields import ( from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \
LocalizedBooleanField, LocalizedFileField
LocalizedCharField,
LocalizedField,
LocalizedFileField,
LocalizedTextField,
)
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
LocalizedField: {"widget": widgets.AdminLocalizedFieldWidget}, LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedCharField: {"widget": widgets.AdminLocalizedCharFieldWidget}, LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget},
LocalizedTextField: {"widget": widgets.AdminLocalizedFieldWidget}, LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget},
LocalizedFileField: {"widget": widgets.AdminLocalizedFileFieldWidget}, LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget},
LocalizedBooleanField: {"widget": widgets.AdminLocalizedBooleanFieldWidget},
} }
@ -20,11 +15,15 @@ class LocalizedFieldsAdminMixin:
"""Mixin for making the fancy widgets work in Django Admin.""" """Mixin for making the fancy widgets work in Django Admin."""
class Media: class Media:
css = {"all": ("localized_fields/localized-fields-admin.css",)} css = {
'all': (
'localized_fields/localized-fields-admin.css',
)
}
js = ( js = (
"admin/js/jquery.init.js", 'admin/js/jquery.init.js',
"localized_fields/localized-fields-admin.js", 'localized_fields/localized-fields-admin.js',
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -9,10 +9,10 @@ from .lookups import LocalizedLookupMixin
class LocalizedFieldsConfig(AppConfig): class LocalizedFieldsConfig(AppConfig):
name = "localized_fields" name = 'localized_fields'
def ready(self): def ready(self):
if getattr(settings, "LOCALIZED_FIELDS_EXPERIMENTAL", True): if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
for _, clazz in inspect.getmembers(lookups): for _, clazz in inspect.getmembers(lookups):
if not inspect.isclass(clazz) or clazz is LocalizedLookupMixin: if not inspect.isclass(clazz) or clazz is LocalizedLookupMixin:
continue continue

View File

@ -1,9 +1,10 @@
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import six, translation
class LocalizedValueDescriptor: class LocalizedValueDescriptor:
"""The descriptor for the localized value attribute on the model instance. """
The descriptor for the localized value attribute on the model instance.
Returns a :see:LocalizedValue when accessed so you can do stuff like:: Returns a :see:LocalizedValue when accessed so you can do stuff like::
>>> from myapp.models import MyModel >>> from myapp.models import MyModel
@ -57,10 +58,8 @@ class LocalizedValueDescriptor:
return instance.__dict__[self.field.name] return instance.__dict__[self.field.name]
def __set__(self, instance, value): def __set__(self, instance, value):
if isinstance(value, str): if isinstance(value, six.string_types):
language = translation.get_language() or settings.LANGUAGE_CODE language = translation.get_language() or settings.LANGUAGE_CODE
self.__get__(instance).set( self.__get__(instance).set(language, value) # pylint: disable=no-member
language, value
) # pylint: disable=no-member
else: else:
instance.__dict__[self.field.name] = value instance.__dict__[self.field.name] = value

View File

@ -1,13 +1,14 @@
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from psqlextra import expressions from psqlextra import expressions
class LocalizedRef(expressions.HStoreRef): class LocalizedRef(expressions.HStoreRef):
"""Expression that selects the value in a field only in the currently """Expression that selects the value in a field only in
active language.""" the currently active language."""
def __init__(self, name: str, lang: str = None): def __init__(self, name: str, lang: str=None):
"""Initializes a new instance of :see:LocalizedRef. """Initializes a new instance of :see:LocalizedRef.
Arguments: Arguments:

View File

@ -1,28 +1,28 @@
from .autoslug_field import LocalizedAutoSlugField
from .boolean_field import LocalizedBooleanField
from .char_field import LocalizedCharField
from .field import LocalizedField from .field import LocalizedField
from .file_field import LocalizedFileField from .autoslug_field import LocalizedAutoSlugField
from .float_field import LocalizedFloatField
from .integer_field import LocalizedIntegerField
from .text_field import LocalizedTextField
from .uniqueslug_field import LocalizedUniqueSlugField from .uniqueslug_field import LocalizedUniqueSlugField
from .char_field import LocalizedCharField
from .text_field import LocalizedTextField
from .file_field import LocalizedFileField
from .integer_field import LocalizedIntegerField
from .float_field import LocalizedFloatField
__all__ = [ __all__ = [
"LocalizedField", 'LocalizedField',
"LocalizedAutoSlugField", 'LocalizedAutoSlugField',
"LocalizedUniqueSlugField", 'LocalizedUniqueSlugField',
"LocalizedCharField", 'LocalizedCharField',
"LocalizedTextField", 'LocalizedTextField',
"LocalizedFileField", 'LocalizedFileField',
"LocalizedIntegerField", 'LocalizedIntegerField',
"LocalizedFloatField", 'LocalizedFloatField'
"LocalizedBooleanField",
] ]
try: try:
from .bleach_field import LocalizedBleachField from .bleach_field import LocalizedBleachField
__all__ += [
__all__ += ["LocalizedBleachField"] 'LocalizedBleachField'
]
except ImportError: except ImportError:
pass pass

View File

@ -1,53 +1,60 @@
import warnings import warnings
from datetime import datetime
from typing import Callable, Tuple, Union from typing import Callable, Tuple, Union
from datetime import datetime
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.utils.text import slugify from django.utils.text import slugify
from ..util import resolve_object_property
from ..value import LocalizedValue
from .field import LocalizedField from .field import LocalizedField
from ..value import LocalizedValue
from ..util import resolve_object_property
class LocalizedAutoSlugField(LocalizedField): class LocalizedAutoSlugField(LocalizedField):
"""Automatically provides slugs for a localized field upon saving.""" """Automatically provides slugs for a localized
field upon saving."""
warnings.warn(
'LocalizedAutoSlug is deprecated and will be removed in the next major version.',
DeprecationWarning
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedAutoSlugField.""" """Initializes a new instance of :see:LocalizedAutoSlugField."""
warnings.warn( self.populate_from = kwargs.pop('populate_from', None)
"LocalizedAutoSlug is deprecated and will be removed in the next major version.", self.include_time = kwargs.pop('include_time', False)
DeprecationWarning,
super(LocalizedAutoSlugField, self).__init__(
*args,
**kwargs
) )
self.populate_from = kwargs.pop("populate_from", None)
self.include_time = kwargs.pop("include_time", False)
super(LocalizedAutoSlugField, self).__init__(*args, **kwargs)
def deconstruct(self): def deconstruct(self):
"""Deconstructs the field into something the database can store.""" """Deconstructs the field into something the database
can store."""
name, path, args, kwargs = super( name, path, args, kwargs = super(
LocalizedAutoSlugField, self LocalizedAutoSlugField, self).deconstruct()
).deconstruct()
kwargs["populate_from"] = self.populate_from kwargs['populate_from'] = self.populate_from
kwargs["include_time"] = self.include_time kwargs['include_time'] = self.include_time
return name, path, args, kwargs return name, path, args, kwargs
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field. """Gets the form field associated with this field.
Because this is a slug field which is automatically populated, Because this is a slug field which is automatically
it should be hidden from the form. populated, it should be hidden from the form.
""" """
defaults = {"form_class": forms.CharField, "required": False} defaults = {
'form_class': forms.CharField,
'required': False
}
defaults.update(kwargs) defaults.update(kwargs)
@ -57,7 +64,8 @@ class LocalizedAutoSlugField(LocalizedField):
return form_field return form_field
def pre_save(self, instance, add: bool): def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built the slug. """Ran just before the model is saved, allows us to built
the slug.
Arguments: Arguments:
instance: instance:
@ -75,19 +83,21 @@ class LocalizedAutoSlugField(LocalizedField):
continue continue
if self.include_time: if self.include_time:
value += "-%s" % datetime.now().microsecond value += '-%s' % datetime.now().microsecond
def is_unique(slug: str, language: str) -> bool: def is_unique(slug: str, language: str) -> bool:
"""Gets whether the specified slug is unique.""" """Gets whether the specified slug is unique."""
unique_filter = {"%s__%s" % (self.name, language): slug} unique_filter = {
'%s__%s' % (self.name, language): slug
}
return ( return not type(instance).objects.filter(**unique_filter).exists()
not type(instance).objects.filter(**unique_filter).exists()
)
slug = self._make_unique_slug( slug = self._make_unique_slug(
slugify(value, allow_unicode=True), lang_code, is_unique slugify(value, allow_unicode=True),
lang_code,
is_unique
) )
slugs.set(lang_code, slug) slugs.set(lang_code, slug)
@ -96,11 +106,9 @@ class LocalizedAutoSlugField(LocalizedField):
return slugs return slugs
@staticmethod @staticmethod
def _make_unique_slug( def _make_unique_slug(slug: str, language: str, is_unique: Callable[[str], bool]) -> str:
slug: str, language: str, is_unique: Callable[[str], bool] """Guarentees that the specified slug is unique by appending
) -> str: a number until it is unique.
"""Guarentees that the specified slug is unique by appending a number
until it is unique.
Arguments: Arguments:
slug: slug:
@ -118,14 +126,14 @@ class LocalizedAutoSlugField(LocalizedField):
unique_slug = slug unique_slug = slug
while not is_unique(unique_slug, language): while not is_unique(unique_slug, language):
unique_slug = "%s-%d" % (slug, index) unique_slug = '%s-%d' % (slug, index)
index += 1 index += 1
return unique_slug return unique_slug
def _get_populate_values(self, instance) -> Tuple[str, str]: def _get_populate_values(self, instance) -> Tuple[str, str]:
"""Gets all values (for each language) from the specified's instance's """Gets all values (for each language) from the
`populate_from` field. specified's instance's `populate_from` field.
Arguments: Arguments:
instance: instance:
@ -139,16 +147,16 @@ class LocalizedAutoSlugField(LocalizedField):
( (
lang_code, lang_code,
self._get_populate_from_value( self._get_populate_from_value(
instance, self.populate_from, lang_code instance,
self.populate_from,
lang_code
), ),
) )
for lang_code, _ in settings.LANGUAGES for lang_code, _ in settings.LANGUAGES
] ]
@staticmethod @staticmethod
def _get_populate_from_value( def _get_populate_from_value(instance, field_name: Union[str, Tuple[str]], language: str):
instance, field_name: Union[str, Tuple[str]], language: str
):
"""Gets the value to create a slug from in the specified language. """Gets the value to create a slug from in the specified language.
Arguments: Arguments:
@ -174,13 +182,11 @@ class LocalizedAutoSlugField(LocalizedField):
return str(value) return str(value)
if isinstance(field_name, tuple) or isinstance(field_name, list): if isinstance(field_name, tuple) or isinstance(field_name, list):
value = "-".join( value = '-'.join([
[ value
value for value in [get_field_value(name) for name in field_name]
for value in [get_field_value(name) for name in field_name] if value
if value ])
]
)
return value return value
return get_field_value(field_name) return get_field_value(field_name)

View File

@ -1,33 +1,18 @@
import html import bleach
from django.conf import settings from django.conf import settings
from django_bleach.utils import get_bleach_default_options
from .field import LocalizedField from .field import LocalizedField
class LocalizedBleachField(LocalizedField): class LocalizedBleachField(LocalizedField):
"""Custom version of :see:BleachField that is actually a """Custom version of :see:BleachField that
:see:LocalizedField.""" is actually a :see:LocalizedField."""
DEFAULT_SHOULD_ESCAPE = True
def __init__(self, *args, escape=True, **kwargs):
"""Initializes a new instance of :see:LocalizedBleachField."""
self.escape = escape
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.escape != self.DEFAULT_SHOULD_ESCAPE:
kwargs["escape"] = self.escape
return name, path, args, kwargs
def pre_save(self, instance, add: bool): def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built the slug. """Ran just before the model is saved, allows us to built
the slug.
Arguments: Arguments:
instance: instance:
@ -38,20 +23,6 @@ class LocalizedBleachField(LocalizedField):
to the database or an update. to the database or an update.
""" """
# the bleach library vendors dependencies and the html5lib
# dependency is incompatible with python 3.9, until that's
# fixed, you cannot use LocalizedBleachField with python 3.9
# sympton:
# ImportError: cannot import name 'Mapping' from 'collections'
try:
import bleach
from django_bleach.utils import get_bleach_default_options
except ImportError:
raise UserWarning(
"LocalizedBleachField is not compatible with Python 3.9 yet."
)
localized_value = getattr(instance, self.attname) localized_value = getattr(instance, self.attname)
if not localized_value: if not localized_value:
return None return None
@ -61,14 +32,9 @@ class LocalizedBleachField(LocalizedField):
if not value: if not value:
continue continue
cleaned_value = bleach.clean(
value if self.escape else html.unescape(value),
**get_bleach_default_options()
)
localized_value.set( localized_value.set(
lang_code, lang_code,
cleaned_value if self.escape else html.unescape(cleaned_value), bleach.clean(value, **get_bleach_default_options())
) )
return localized_value return localized_value

View File

@ -1,110 +0,0 @@
from typing import Dict, Optional, Union
from django.conf import settings
from django.db.utils import IntegrityError
from ..forms import LocalizedBooleanFieldForm
from ..value import LocalizedBooleanValue, LocalizedValue
from .field import LocalizedField
class LocalizedBooleanField(LocalizedField):
"""Stores booleans as a localized value."""
attr_class = LocalizedBooleanValue
@classmethod
def from_db_value(cls, value, *_) -> Optional[LocalizedBooleanValue]:
db_value = super().from_db_value(value)
if db_value is None:
return db_value
if isinstance(db_value, str):
if db_value.lower() == "true":
return True
return False
if not isinstance(db_value, LocalizedValue):
return db_value
return cls._convert_localized_value(db_value)
def to_python(
self, value: Union[Dict[str, str], str, None]
) -> LocalizedBooleanValue:
"""Converts the value from a database value into a Python value."""
db_value = super().to_python(value)
return self._convert_localized_value(db_value)
def get_prep_value(self, value: LocalizedBooleanValue) -> dict:
"""Gets the value in a format to store into the database."""
# apply default values
default_values = LocalizedBooleanValue(self.default)
if isinstance(value, LocalizedBooleanValue):
for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code)
if local_value is None:
value.set(lang_code, default_values.get(lang_code, None))
prepped_value = super().get_prep_value(value)
if prepped_value is None:
return None
# make sure all values are proper values to be converted to bool
for lang_code, _ in settings.LANGUAGES:
local_value = prepped_value[lang_code]
if local_value is not None and local_value.lower() not in (
"false",
"true",
):
raise IntegrityError(
'non-boolean value in column "%s.%s" violates '
"boolean constraint" % (self.name, lang_code)
)
# convert to a string before saving because the underlying
# type is hstore, which only accept strings
prepped_value[lang_code] = (
str(local_value) if local_value is not None else None
)
return prepped_value
def formfield(self, **kwargs):
"""Gets the form field associated with this field."""
defaults = {"form_class": LocalizedBooleanFieldForm}
defaults.update(kwargs)
return super().formfield(**defaults)
@staticmethod
def _convert_localized_value(
value: LocalizedValue,
) -> LocalizedBooleanValue:
"""Converts from :see:LocalizedValue to :see:LocalizedBooleanValue."""
integer_values = {}
for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code, None)
if isinstance(local_value, str):
if local_value.lower() == "false":
local_value = False
elif local_value.lower() == "true":
local_value = True
else:
raise ValueError(
f"Could not convert value {local_value} to boolean."
)
integer_values[lang_code] = local_value
elif local_value is not None:
raise TypeError(
f"Expected value of type str instead of {type(local_value)}."
)
return LocalizedBooleanValue(integer_values)

View File

@ -1,6 +1,6 @@
from ..forms import LocalizedCharFieldForm from ..forms import LocalizedCharFieldForm
from ..value import LocalizedStringValue
from .field import LocalizedField from .field import LocalizedField
from ..value import LocalizedStringValue
class LocalizedCharField(LocalizedField): class LocalizedCharField(LocalizedField):
@ -8,7 +8,9 @@ class LocalizedCharField(LocalizedField):
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field.""" """Gets the form field associated with this field."""
defaults = {"form_class": LocalizedCharFieldForm} defaults = {
'form_class': LocalizedCharFieldForm
}
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)

View File

@ -1,22 +1,22 @@
import json import json
from typing import List, Optional, Union from typing import Union, List, Optional
from django.conf import settings from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from psqlextra.fields import HStoreField from psqlextra.fields import HStoreField
from ..descriptor import LocalizedValueDescriptor
from ..forms import LocalizedFieldForm from ..forms import LocalizedFieldForm
from ..value import LocalizedValue from ..value import LocalizedValue
from ..descriptor import LocalizedValueDescriptor
class LocalizedField(HStoreField): class LocalizedField(HStoreField):
"""A field that has the same value in multiple languages. """A field that has the same value in multiple languages.
Internally this is stored as a :see:HStoreField where there is a key Internally this is stored as a :see:HStoreField where there
for every language. is a key for every language."""
"""
Meta = None Meta = None
@ -27,27 +27,17 @@ class LocalizedField(HStoreField):
# The descriptor to use for accessing the attribute off of the class. # The descriptor to use for accessing the attribute off of the class.
descriptor_class = LocalizedValueDescriptor descriptor_class = LocalizedValueDescriptor
def __init__( def __init__(self, *args, required: Union[bool, List[str]]=None, **kwargs):
self,
*args,
required: Optional[Union[bool, List[str]]] = None,
blank: bool = False,
**kwargs
):
"""Initializes a new instance of :see:LocalizedField.""" """Initializes a new instance of :see:LocalizedField."""
if (required is None and blank) or required is False: super(LocalizedField, self).__init__(*args, required=required, **kwargs)
self.required = []
elif required is None and not blank:
self.required = [settings.LANGUAGE_CODE]
elif required is True:
self.required = [lang_code for lang_code, _ in settings.LANGUAGES]
else:
self.required = required
super(LocalizedField, self).__init__( if (self.required is None and self.blank) or self.required is False:
*args, required=self.required, blank=blank, **kwargs self.required = []
) elif self.required is None and not self.blank:
self.required = [settings.LANGUAGE_CODE]
elif self.required is True:
self.required = [lang_code for lang_code, _ in settings.LANGUAGES]
def contribute_to_class(self, model, name, **kwargs): def contribute_to_class(self, model, name, **kwargs):
"""Adds this field to the specifed model. """Adds this field to the specifed model.
@ -64,7 +54,8 @@ class LocalizedField(HStoreField):
@classmethod @classmethod
def from_db_value(cls, value, *_) -> Optional[LocalizedValue]: def from_db_value(cls, value, *_) -> Optional[LocalizedValue]:
"""Turns the specified database value into its Python equivalent. """Turns the specified database value into its Python
equivalent.
Arguments: Arguments:
value: value:
@ -77,7 +68,7 @@ class LocalizedField(HStoreField):
""" """
if not value: if not value:
if getattr(settings, "LOCALIZED_FIELDS_EXPERIMENTAL", True): if getattr(settings, 'LOCALIZED_FIELDS_EXPERIMENTAL', False):
return None return None
else: else:
return cls.attr_class() return cls.attr_class()
@ -107,7 +98,8 @@ class LocalizedField(HStoreField):
return cls.attr_class(value) return cls.attr_class(value)
def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: def to_python(self, value: Union[dict, str, None]) -> LocalizedValue:
"""Turns the specified database value into its Python equivalent. """Turns the specified database value into its Python
equivalent.
Arguments: Arguments:
value: value:
@ -132,7 +124,8 @@ class LocalizedField(HStoreField):
return self.attr_class(deserialized_value) return self.attr_class(deserialized_value)
def get_prep_value(self, value: LocalizedValue) -> dict: def get_prep_value(self, value: LocalizedValue) -> dict:
"""Turns the specified value into something the database can store. """Turns the specified value into something the database
can store.
If an illegal value (non-LocalizedValue instance) is If an illegal value (non-LocalizedValue instance) is
specified, we'll treat it as an empty :see:LocalizedValue specified, we'll treat it as an empty :see:LocalizedValue
@ -168,8 +161,8 @@ class LocalizedField(HStoreField):
) )
def clean(self, value, *_): def clean(self, value, *_):
"""Cleans the specified value into something we can store in the """Cleans the specified value into something we
database. can store in the database.
For example, when all the language fields are For example, when all the language fields are
left empty, and the field is allowed to be null, left empty, and the field is allowed to be null,
@ -202,7 +195,7 @@ class LocalizedField(HStoreField):
def validate(self, value: LocalizedValue, *_): def validate(self, value: LocalizedValue, *_):
"""Validates that the values has been filled in for all required """Validates that the values has been filled in for all required
languages. languages
Exceptions are raises in order to notify the user Exceptions are raises in order to notify the user
of invalid values. of invalid values.
@ -219,17 +212,15 @@ class LocalizedField(HStoreField):
lang_val = getattr(value, settings.LANGUAGE_CODE) lang_val = getattr(value, settings.LANGUAGE_CODE)
if lang_val is None: if lang_val is None:
raise IntegrityError( raise IntegrityError('null value in column "%s.%s" violates '
'null value in column "%s.%s" violates ' 'not-null constraint' % (self.name, lang))
"not-null constraint" % (self.name, lang)
)
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field.""" """Gets the form field associated with this field."""
defaults = dict( defaults = dict(
form_class=LocalizedFieldForm, form_class=LocalizedFieldForm,
required=False if self.blank else self.required, required=False if self.blank else self.required
) )
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)

View File

@ -1,30 +1,31 @@
import datetime
import json import json
import datetime
import posixpath import posixpath
from django.core.files import File from django.core.files import File
from django.core.files.storage import default_storage
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from django.utils.encoding import force_str from django.utils import six
from django.core.files.storage import default_storage
from django.utils.encoding import force_str, force_text
from localized_fields.fields import LocalizedField from localized_fields.fields import LocalizedField
from localized_fields.fields.field import LocalizedValueDescriptor from localized_fields.fields.field import LocalizedValueDescriptor
from localized_fields.value import LocalizedValue from localized_fields.value import LocalizedValue
from ..forms import LocalizedFileFieldForm
from ..value import LocalizedFileValue from ..value import LocalizedFileValue
from ..forms import LocalizedFileFieldForm
class LocalizedFieldFile(FieldFile): class LocalizedFieldFile(FieldFile):
def __init__(self, instance, field, name, lang): def __init__(self, instance, field, name, lang):
super().__init__(instance, field, name) super().__init__(instance, field, name)
self.lang = lang self.lang = lang
def save(self, name, content, save=True): def save(self, name, content, save=True):
name = self.field.generate_filename(self.instance, name, self.lang) name = self.field.generate_filename(self.instance, name, self.lang)
self.name = self.storage.save( self.name = self.storage.save(name, content,
name, content, max_length=self.field.max_length max_length=self.field.max_length)
)
self._committed = True self._committed = True
if save: if save:
@ -36,7 +37,7 @@ class LocalizedFieldFile(FieldFile):
if not self: if not self:
return return
if hasattr(self, "_file"): if hasattr(self, '_file'):
self.close() self.close()
del self.file del self.file
@ -55,33 +56,28 @@ class LocalizedFileValueDescriptor(LocalizedValueDescriptor):
def __get__(self, instance, cls=None): def __get__(self, instance, cls=None):
value = super().__get__(instance, cls) value = super().__get__(instance, cls)
for lang, file in value.__dict__.items(): for lang, file in value.__dict__.items():
if isinstance(file, str) or file is None: if isinstance(file, six.string_types) or file is None:
file = self.field.value_class(instance, self.field, file, lang) file = self.field.value_class(instance, self.field, file, lang)
value.set(lang, file) value.set(lang, file)
elif isinstance(file, File) and not isinstance( elif isinstance(file, File) and \
file, LocalizedFieldFile not isinstance(file, LocalizedFieldFile):
): file_copy = self.field.value_class(instance, self.field,
file_copy = self.field.value_class( file.name, lang)
instance, self.field, file.name, lang
)
file_copy.file = file file_copy.file = file
file_copy._committed = False file_copy._committed = False
value.set(lang, file_copy) value.set(lang, file_copy)
elif isinstance(file, LocalizedFieldFile) and not hasattr( elif isinstance(file, LocalizedFieldFile) and \
file, "field" not hasattr(file, 'field'):
):
file.instance = instance file.instance = instance
file.field = self.field file.field = self.field
file.storage = self.field.storage file.storage = self.field.storage
file.lang = lang file.lang = lang
# Make sure that the instance is correct. # Make sure that the instance is correct.
elif ( elif isinstance(file, LocalizedFieldFile) \
isinstance(file, LocalizedFieldFile) and instance is not file.instance:
and instance is not file.instance
):
file.instance = instance file.instance = instance
file.lang = lang file.lang = lang
return value return value
@ -92,9 +88,8 @@ class LocalizedFileField(LocalizedField):
attr_class = LocalizedFileValue attr_class = LocalizedFileValue
value_class = LocalizedFieldFile value_class = LocalizedFieldFile
def __init__( def __init__(self, verbose_name=None, name=None, upload_to='', storage=None,
self, verbose_name=None, name=None, upload_to="", storage=None, **kwargs **kwargs):
):
self.storage = storage or default_storage self.storage = storage or default_storage
self.upload_to = upload_to self.upload_to = upload_to
@ -103,9 +98,9 @@ class LocalizedFileField(LocalizedField):
def deconstruct(self): def deconstruct(self):
name, path, args, kwargs = super().deconstruct() name, path, args, kwargs = super().deconstruct()
kwargs["upload_to"] = self.upload_to kwargs['upload_to'] = self.upload_to
if self.storage is not default_storage: if self.storage is not default_storage:
kwargs["storage"] = self.storage kwargs['storage'] = self.storage
return name, path, args, kwargs return name, path, args, kwargs
def get_prep_value(self, value): def get_prep_value(self, value):
@ -115,11 +110,11 @@ class LocalizedFileField(LocalizedField):
prep_value = LocalizedValue() prep_value = LocalizedValue()
for k, v in value.__dict__.items(): for k, v in value.__dict__.items():
if v is None: if v is None:
prep_value.set(k, "") prep_value.set(k, '')
else: else:
# Need to convert File objects provided via a form to # Need to convert File objects provided via a form to
# unicode for database insertion # unicode for database insertion
prep_value.set(k, str(v)) prep_value.set(k, six.text_type(v))
return super().get_prep_value(prep_value) return super().get_prep_value(prep_value)
return super().get_prep_value(value) return super().get_prep_value(value)
@ -137,7 +132,7 @@ class LocalizedFileField(LocalizedField):
filename = self.upload_to(instance, filename, lang) filename = self.upload_to(instance, filename, lang)
else: else:
now = datetime.datetime.now() now = datetime.datetime.now()
dirname = force_str(now.strftime(force_str(self.upload_to))) dirname = force_text(now.strftime(force_str(self.upload_to)))
dirname = dirname.format(lang=lang) dirname = dirname.format(lang=lang)
filename = posixpath.join(dirname, filename) filename = posixpath.join(dirname, filename)
return self.storage.generate_filename(filename) return self.storage.generate_filename(filename)
@ -146,17 +141,18 @@ class LocalizedFileField(LocalizedField):
if isinstance(data, LocalizedValue): if isinstance(data, LocalizedValue):
for k, v in data.__dict__.items(): for k, v in data.__dict__.items():
if v is not None and not v: if v is not None and not v:
data.set(k, "") data.set(k, '')
setattr(instance, self.name, data) setattr(instance, self.name, data)
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {"form_class": LocalizedFileFieldForm} defaults = {'form_class': LocalizedFileFieldForm}
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)
def value_to_string(self, obj): def value_to_string(self, obj):
value = self.value_from_object(obj) value = self.value_from_object(obj)
if isinstance(value, LocalizedFileValue): if isinstance(value, LocalizedFileValue):
return json.dumps({k: v.name for k, v in value.__dict__.items()}) return json.dumps({k: v.name for k, v
in value.__dict__.items()})
else: else:
return super().value_to_string(obj) return super().value_to_string(obj)

View File

@ -1,11 +1,11 @@
from typing import Dict, Optional, Union from typing import Optional, Union, Dict
from django.conf import settings from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from ..forms import LocalizedIntegerFieldForm
from ..value import LocalizedFloatValue, LocalizedValue
from .field import LocalizedField from .field import LocalizedField
from ..value import LocalizedValue, LocalizedFloatValue
from ..forms import LocalizedIntegerFieldForm
class LocalizedFloatField(LocalizedField): class LocalizedFloatField(LocalizedField):
@ -27,9 +27,7 @@ class LocalizedFloatField(LocalizedField):
return cls._convert_localized_value(db_value) return cls._convert_localized_value(db_value)
def to_python( def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedFloatValue:
self, value: Union[Dict[str, int], int, None]
) -> LocalizedFloatValue:
"""Converts the value from a database value into a Python value.""" """Converts the value from a database value into a Python value."""
db_value = super().to_python(value) db_value = super().to_python(value)
@ -57,22 +55,20 @@ class LocalizedFloatField(LocalizedField):
if local_value is not None: if local_value is not None:
float(local_value) float(local_value)
except (TypeError, ValueError): except (TypeError, ValueError):
raise IntegrityError( raise IntegrityError('non-float value in column "%s.%s" violates '
'non-float value in column "%s.%s" violates ' 'float constraint' % (self.name, lang_code))
"float constraint" % (self.name, lang_code)
)
# convert to a string before saving because the underlying # convert to a string before saving because the underlying
# type is hstore, which only accept strings # type is hstore, which only accept strings
prepped_value[lang_code] = ( prepped_value[lang_code] = str(local_value) if local_value is not None else None
str(local_value) if local_value is not None else None
)
return prepped_value return prepped_value
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field.""" """Gets the form field associated with this field."""
defaults = {"form_class": LocalizedIntegerFieldForm} defaults = {
'form_class': LocalizedIntegerFieldForm
}
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)
@ -84,7 +80,7 @@ class LocalizedFloatField(LocalizedField):
float_values = {} float_values = {}
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code, None) local_value = value.get(lang_code, None)
if local_value is None or local_value.strip() == "": if local_value is None or local_value.strip() == '':
local_value = None local_value = None
try: try:

View File

@ -1,21 +1,11 @@
from typing import Dict, Optional, Union from typing import Optional, Union, Dict
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields.hstore import KeyTransform
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from ..forms import LocalizedIntegerFieldForm
from ..value import LocalizedIntegerValue, LocalizedValue
from .field import LocalizedField from .field import LocalizedField
from ..value import LocalizedValue, LocalizedIntegerValue
from ..forms import LocalizedIntegerFieldForm
class LocalizedIntegerFieldKeyTransform(KeyTransform):
"""Transform that selects a single key from a hstore value and casts it to
an integer."""
def as_sql(self, compiler, connection):
sql, params = super().as_sql(compiler, connection)
return f"{sql}::integer", params
class LocalizedIntegerField(LocalizedField): class LocalizedIntegerField(LocalizedField):
@ -23,28 +13,12 @@ class LocalizedIntegerField(LocalizedField):
attr_class = LocalizedIntegerValue attr_class = LocalizedIntegerValue
def get_transform(self, name):
"""Gets the transformation to apply when selecting this value.
This is where the SQL expression to grab a single is added and
the cast to integer so that sorting by a hstore value works as
expected.
"""
def _transform(*args, **kwargs):
return LocalizedIntegerFieldKeyTransform(name, *args, **kwargs)
return _transform
@classmethod @classmethod
def from_db_value(cls, value, *_) -> Optional[LocalizedIntegerValue]: def from_db_value(cls, value, *_) -> Optional[LocalizedIntegerValue]:
db_value = super().from_db_value(value) db_value = super().from_db_value(value)
if db_value is None: if db_value is None:
return db_value return db_value
if isinstance(db_value, str):
return int(db_value)
# if we were used in an expression somehow then it might be # if we were used in an expression somehow then it might be
# that we're returning an individual value or an array, so # that we're returning an individual value or an array, so
# we should not convert that into an :see:LocalizedIntegerValue # we should not convert that into an :see:LocalizedIntegerValue
@ -53,9 +27,7 @@ class LocalizedIntegerField(LocalizedField):
return cls._convert_localized_value(db_value) return cls._convert_localized_value(db_value)
def to_python( def to_python(self, value: Union[Dict[str, int], int, None]) -> LocalizedIntegerValue:
self, value: Union[Dict[str, int], int, None]
) -> LocalizedIntegerValue:
"""Converts the value from a database value into a Python value.""" """Converts the value from a database value into a Python value."""
db_value = super().to_python(value) db_value = super().to_python(value)
@ -83,36 +55,32 @@ class LocalizedIntegerField(LocalizedField):
if local_value is not None: if local_value is not None:
int(local_value) int(local_value)
except (TypeError, ValueError): except (TypeError, ValueError):
raise IntegrityError( raise IntegrityError('non-integer value in column "%s.%s" violates '
'non-integer value in column "%s.%s" violates ' 'integer constraint' % (self.name, lang_code))
"integer constraint" % (self.name, lang_code)
)
# convert to a string before saving because the underlying # convert to a string before saving because the underlying
# type is hstore, which only accept strings # type is hstore, which only accept strings
prepped_value[lang_code] = ( prepped_value[lang_code] = str(local_value) if local_value is not None else None
str(local_value) if local_value is not None else None
)
return prepped_value return prepped_value
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field.""" """Gets the form field associated with this field."""
defaults = {"form_class": LocalizedIntegerFieldForm} defaults = {
'form_class': LocalizedIntegerFieldForm
}
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)
@staticmethod @staticmethod
def _convert_localized_value( def _convert_localized_value(value: LocalizedValue) -> LocalizedIntegerValue:
value: LocalizedValue,
) -> LocalizedIntegerValue:
"""Converts from :see:LocalizedValue to :see:LocalizedIntegerValue.""" """Converts from :see:LocalizedValue to :see:LocalizedIntegerValue."""
integer_values = {} integer_values = {}
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
local_value = value.get(lang_code, None) local_value = value.get(lang_code, None)
if local_value is None or local_value.strip() == "": if local_value is None or local_value.strip() == '':
local_value = None local_value = None
try: try:

View File

@ -6,7 +6,9 @@ class LocalizedTextField(LocalizedCharField):
def formfield(self, **kwargs): def formfield(self, **kwargs):
"""Gets the form field associated with this field.""" """Gets the form field associated with this field."""
defaults = {"form_class": LocalizedTextFieldForm} defaults = {
'form_class': LocalizedTextFieldForm
}
defaults.update(kwargs) defaults.update(kwargs)
return super().formfield(**defaults) return super().formfield(**defaults)

View File

@ -1,16 +1,17 @@
from datetime import datetime from datetime import datetime
from django.core.exceptions import ImproperlyConfigured
from django.utils.text import slugify from django.utils.text import slugify
from django.core.exceptions import ImproperlyConfigured
from ..mixins import AtomicSlugRetryMixin
from ..util import get_language_codes
from ..value import LocalizedValue
from .autoslug_field import LocalizedAutoSlugField from .autoslug_field import LocalizedAutoSlugField
from ..util import get_language_codes
from ..mixins import AtomicSlugRetryMixin
from ..value import LocalizedValue
class LocalizedUniqueSlugField(LocalizedAutoSlugField): class LocalizedUniqueSlugField(LocalizedAutoSlugField):
"""Automatically provides slugs for a localized field upon saving.". """Automatically provides slugs for a localized
field upon saving."
An improved version of :see:LocalizedAutoSlugField, An improved version of :see:LocalizedAutoSlugField,
which adds: which adds:
@ -21,45 +22,35 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
When in doubt, use this over :see:LocalizedAutoSlugField. When in doubt, use this over :see:LocalizedAutoSlugField.
Inherit from :see:AtomicSlugRetryMixin in your model to Inherit from :see:AtomicSlugRetryMixin in your model to
make this field work properly. make this field work properly.
By default, this creates a new slug if the field(s) specified
in `populate_from` are changed. Set `immutable=True` to get
immutable slugs.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedUniqueSlugField.""" """Initializes a new instance of :see:LocalizedUniqueSlugField."""
kwargs["uniqueness"] = kwargs.pop("uniqueness", get_language_codes()) kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
self.enabled = kwargs.pop("enabled", True) super(LocalizedUniqueSlugField, self).__init__(
self.immutable = kwargs.pop("immutable", False) *args,
**kwargs
)
super(LocalizedUniqueSlugField, self).__init__(*args, **kwargs) self.populate_from = kwargs.pop('populate_from')
self.include_time = kwargs.pop('include_time', False)
self.populate_from = kwargs.pop("populate_from")
self.include_time = kwargs.pop("include_time", False)
def deconstruct(self): def deconstruct(self):
"""Deconstructs the field into something the database can store.""" """Deconstructs the field into something the database
can store."""
name, path, args, kwargs = super( name, path, args, kwargs = super(
LocalizedUniqueSlugField, self LocalizedUniqueSlugField, self).deconstruct()
).deconstruct()
kwargs["populate_from"] = self.populate_from
kwargs["include_time"] = self.include_time
if self.enabled is False:
kwargs["enabled"] = self.enabled
if self.immutable is True:
kwargs["immutable"] = self.immutable
kwargs['populate_from'] = self.populate_from
kwargs['include_time'] = self.include_time
return name, path, args, kwargs return name, path, args, kwargs
def pre_save(self, instance, add: bool): def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built the slug. """Ran just before the model is saved, allows us to built
the slug.
Arguments: Arguments:
instance: instance:
@ -73,17 +64,11 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
The localized slug that was generated. The localized slug that was generated.
""" """
if not self.enabled:
return getattr(instance, self.name)
if not isinstance(instance, AtomicSlugRetryMixin): if not isinstance(instance, AtomicSlugRetryMixin):
raise ImproperlyConfigured( raise ImproperlyConfigured((
( 'Model \'%s\' does not inherit from AtomicSlugRetryMixin. '
"Model '%s' does not inherit from AtomicSlugRetryMixin. " 'Without this, the LocalizedUniqueSlugField will not work.'
"Without this, the LocalizedUniqueSlugField will not work." ) % type(instance).__name__)
)
% type(instance).__name__
)
slugs = LocalizedValue() slugs = LocalizedValue()
@ -93,30 +78,25 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField):
slug = slugify(value, allow_unicode=True) slug = slugify(value, allow_unicode=True)
current_slug = getattr(instance, self.name).get(lang_code)
if current_slug and self.immutable:
slugs.set(lang_code, current_slug)
continue
# verify whether it's needed to re-generate a slug, # verify whether it's needed to re-generate a slug,
# if not, re-use the same slug # if not, re-use the same slug
if instance.pk is not None: if instance.pk is not None:
current_slug = getattr(instance, self.name).get(lang_code)
if current_slug is not None: if current_slug is not None:
current_slug_end_index = current_slug.rfind("-") stripped_slug = current_slug[0:current_slug.rfind('-')]
stripped_slug = current_slug[0:current_slug_end_index]
if slug == stripped_slug: if slug == stripped_slug:
slugs.set(lang_code, current_slug) slugs.set(lang_code, current_slug)
continue continue
if self.include_time: if self.include_time:
slug += "-%d" % datetime.now().microsecond slug += '-%d' % datetime.now().microsecond
retries = getattr(instance, "retries", 0) retries = getattr(instance, 'retries', 0)
if retries > 0: if retries > 0:
# do not add another - if we already added time # do not add another - if we already added time
if not self.include_time: if not self.include_time:
slug += "-" slug += '-'
slug += "%d" % retries slug += '%d' % retries
slugs.set(lang_code, slug) slugs.set(lang_code, slug)

View File

@ -5,46 +5,30 @@ from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.widgets import FILE_INPUT_CONTRADICTION from django.forms.widgets import FILE_INPUT_CONTRADICTION
from .value import ( from .value import LocalizedValue, LocalizedStringValue, \
LocalizedBooleanValue, LocalizedFileValue, LocalizedIntegerValue
LocalizedFileValue, from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \
LocalizedIntegerValue, LocalizedFileWidget, AdminLocalizedIntegerFieldWidget
LocalizedStringValue,
LocalizedValue,
)
from .widgets import (
AdminLocalizedBooleanFieldWidget,
AdminLocalizedIntegerFieldWidget,
LocalizedCharFieldWidget,
LocalizedFieldWidget,
LocalizedFileWidget,
)
class LocalizedFieldForm(forms.MultiValueField): class LocalizedFieldForm(forms.MultiValueField):
"""Form for a localized field, allows editing the field in multiple """Form for a localized field, allows editing
languages.""" the field in multiple languages."""
widget = LocalizedFieldWidget widget = LocalizedFieldWidget
field_class = forms.fields.CharField field_class = forms.fields.CharField
value_class = LocalizedValue value_class = LocalizedValue
def __init__( def __init__(self, *args, required: Union[bool, List[str]]=False, **kwargs):
self, *args, required: Union[bool, List[str]] = False, **kwargs
):
"""Initializes a new instance of :see:LocalizedFieldForm.""" """Initializes a new instance of :see:LocalizedFieldForm."""
fields = [] fields = []
# Do not print initial value in html in the form of a hidden input. This will result in loss of information
kwargs["show_hidden_initial"] = False
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
field_options = dict( field_options = dict(
required=required required=required if type(required) is bool else (lang_code in
if type(required) is bool required),
else (lang_code in required), label=lang_code
label=lang_code,
) )
fields.append(self.field_class(**field_options)) fields.append(self.field_class(**field_options))
@ -52,17 +36,16 @@ class LocalizedFieldForm(forms.MultiValueField):
fields, fields,
required=required if type(required) is bool else True, required=required if type(required) is bool else True,
require_all_fields=False, require_all_fields=False,
*args, *args, **kwargs
**kwargs
) )
# set 'required' attribute for each widget separately # set 'required' attribute for each widget separately
for field, widget in zip(self.fields, self.widget.widgets): for field, widget in zip(self.fields, self.widget.widgets):
widget.is_required = field.required widget.is_required = field.required
def compress(self, value: List[str]) -> LocalizedValue: def compress(self, value: List[str]) -> value_class:
"""Compresses the values from individual fields into a single """Compresses the values from individual fields
:see:LocalizedValue instance. into a single :see:LocalizedValue instance.
Arguments: Arguments:
value: value:
@ -82,50 +65,41 @@ class LocalizedFieldForm(forms.MultiValueField):
class LocalizedCharFieldForm(LocalizedFieldForm): class LocalizedCharFieldForm(LocalizedFieldForm):
"""Form for a localized char field, allows editing the field in multiple """Form for a localized char field, allows editing
languages.""" the field in multiple languages."""
widget = LocalizedCharFieldWidget widget = LocalizedCharFieldWidget
value_class = LocalizedStringValue value_class = LocalizedStringValue
class LocalizedTextFieldForm(LocalizedFieldForm): class LocalizedTextFieldForm(LocalizedFieldForm):
"""Form for a localized text field, allows editing the field in multiple """Form for a localized text field, allows editing
languages.""" the field in multiple languages."""
value_class = LocalizedStringValue value_class = LocalizedStringValue
class LocalizedIntegerFieldForm(LocalizedFieldForm): class LocalizedIntegerFieldForm(LocalizedFieldForm):
"""Form for a localized integer field, allows editing the field in multiple """Form for a localized integer field, allows editing
languages.""" the field in multiple languages."""
widget = AdminLocalizedIntegerFieldWidget widget = AdminLocalizedIntegerFieldWidget
value_class = LocalizedIntegerValue value_class = LocalizedIntegerValue
class LocalizedBooleanFieldForm(LocalizedFieldForm, forms.BooleanField):
"""Form for a localized boolean field, allows editing the field in multiple
languages."""
widget = AdminLocalizedBooleanFieldWidget
field_class = forms.fields.BooleanField
value_class = LocalizedBooleanValue
class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField): class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
"""Form for a localized file field, allows editing the field in multiple """Form for a localized file field, allows editing
languages.""" the field in multiple languages."""
widget = LocalizedFileWidget widget = LocalizedFileWidget
field_class = forms.fields.FileField field_class = forms.fields.FileField
value_class = LocalizedFileValue value_class = LocalizedFileValue
def clean(self, value, initial=None): def clean(self, value, initial=None):
"""Most part of this method is a copy of """
django.forms.MultiValueField.clean, with the exception of initial value Most part of this method is a copy of
handling (this need for correct processing FileField's). django.forms.MultiValueField.clean, with the exception of initial
value handling (this need for correct processing FileField's).
All original comments saved. All original comments saved.
""" """
if initial is None: if initial is None:
@ -137,44 +111,40 @@ class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField):
clean_data = [] clean_data = []
errors = [] errors = []
if not value or isinstance(value, (list, tuple)): if not value or isinstance(value, (list, tuple)):
is_empty = [v for v in value if v not in self.empty_values] if (not value or not [v for v in value if
if (not value or not is_empty) and (not initial or not is_empty): v not in self.empty_values]) \
and (not initial or not [v for v in initial if
v not in self.empty_values]):
if self.required: if self.required:
raise ValidationError( raise ValidationError(self.error_messages['required'],
self.error_messages["required"], code="required" code='required')
)
else: else:
raise ValidationError( raise ValidationError(self.error_messages['invalid'],
self.error_messages["invalid"], code="invalid" code='invalid')
)
for i, field in enumerate(self.fields): for i, field in enumerate(self.fields):
try: try:
field_value = value[i] field_value = value[i]
except IndexError: except IndexError:
field_value = None field_value = None
try: try:
field_initial = initial[i] field_initial = initial[i]
except IndexError: except IndexError:
field_initial = None field_initial = None
if ( if field_value in self.empty_values and \
field_value in self.empty_values field_initial in self.empty_values:
and field_initial in self.empty_values
):
if self.require_all_fields: if self.require_all_fields:
# Raise a 'required' error if the MultiValueField is # Raise a 'required' error if the MultiValueField is
# required and any field is empty. # required and any field is empty.
if self.required: if self.required:
raise ValidationError( raise ValidationError(self.error_messages['required'],
self.error_messages["required"], code="required" code='required')
)
elif field.required: elif field.required:
# Otherwise, add an 'incomplete' error to the list of # Otherwise, add an 'incomplete' error to the list of
# collected errors and skip field cleaning, if a required # collected errors and skip field cleaning, if a required
# field is empty. # field is empty.
if field.error_messages["incomplete"] not in errors: if field.error_messages['incomplete'] not in errors:
errors.append(field.error_messages["incomplete"]) errors.append(field.error_messages['incomplete'])
continue continue
try: try:
clean_data.append(field.clean(field_value, field_initial)) clean_data.append(field.clean(field_value, field_initial))

View File

@ -1,72 +1,22 @@
from django.conf import settings from django.conf import settings
from django.contrib.postgres.fields.hstore import KeyTransform from django.contrib.postgres.fields.hstore import KeyTransform
from django.contrib.postgres.lookups import ( from django.contrib.postgres.lookups import (SearchLookup, TrigramSimilar,
SearchLookup, Unaccent)
TrigramSimilar, from django.db.models.expressions import Col
Unaccent, from django.db.models.lookups import (Contains, EndsWith, Exact, IContains,
) IEndsWith, IExact, In, IRegex, IsNull,
from django.db.models import TextField, Transform IStartsWith, Regex, StartsWith)
from django.db.models.expressions import Col, Func, Value
from django.db.models.functions import Coalesce
from django.db.models.lookups import (
Contains,
EndsWith,
Exact,
IContains,
IEndsWith,
IExact,
In,
IRegex,
IsNull,
IStartsWith,
Regex,
StartsWith,
)
from django.utils import translation from django.utils import translation
from psqlextra.expressions import HStoreColumn
from .fields import LocalizedField
try:
from django.db.models.functions import NullIf
except ImportError:
# for Django < 2.2
class NullIf(Func):
function = "NULLIF"
arity = 2
class LocalizedLookupMixin: class LocalizedLookupMixin():
def process_lhs(self, qn, connection): def process_lhs(self, qn, connection):
# If the LHS is already a reference to a specific hstore key, there if isinstance(self.lhs, Col):
# is nothing to be done since it already references as specific language. language = translation.get_language() or settings.LANGUAGE_CODE
if isinstance(self.lhs, HStoreColumn) or isinstance( self.lhs = KeyTransform(language, self.lhs)
self.lhs, KeyTransform
):
return super().process_lhs(qn, connection)
# If this is something custom expression, we don't really know how to
# handle that, so we better do nothing.
if not isinstance(self.lhs, Col):
return super().process_lhs(qn, connection)
# Select the key for the current language. We do this so that
#
# myfield__<lookup>=
#
# Is converted into:
#
# myfield__<lookup>__<current language>=
language = translation.get_language() or settings.LANGUAGE_CODE
self.lhs = KeyTransform(language, self.lhs)
return super().process_lhs(qn, connection) return super().process_lhs(qn, connection)
def get_prep_lookup(self): def get_prep_lookup(self):
# Django 4.0 removed the ability for isnull fields to be something
# other than a bool We should NOT convert them to strings
if isinstance(self.rhs, bool):
return self.rhs
return str(self.rhs) return str(self.rhs)
@ -128,43 +78,3 @@ class LocalizedRegexWith(LocalizedLookupMixin, Regex):
class LocalizedIRegexWith(LocalizedLookupMixin, IRegex): class LocalizedIRegexWith(LocalizedLookupMixin, IRegex):
pass pass
@LocalizedField.register_lookup
class ActiveRefLookup(Transform):
output_field = TextField()
lookup_name = "active_ref"
arity = None
def as_sql(self, compiler, connection):
language = translation.get_language() or settings.LANGUAGE_CODE
return KeyTransform(language, self.lhs).as_sql(compiler, connection)
@LocalizedField.register_lookup
class TranslatedRefLookup(Transform):
output_field = TextField()
lookup_name = "translated_ref"
arity = None
def as_sql(self, compiler, connection):
language = translation.get_language()
fallback_config = getattr(settings, "LOCALIZED_FIELDS_FALLBACKS", {})
target_languages = fallback_config.get(language, [])
if not target_languages and language != settings.LANGUAGE_CODE:
target_languages.append(settings.LANGUAGE_CODE)
if language:
target_languages.insert(0, language)
if len(target_languages) > 1:
return Coalesce(
*[
NullIf(KeyTransform(language, self.lhs), Value(""))
for language in target_languages
]
).as_sql(compiler, connection)
return KeyTransform(target_languages[0], self.lhs).as_sql(
compiler, connection
)

View File

@ -6,6 +6,9 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [] dependencies = [
]
operations = [HStoreExtension()] operations = [
HStoreExtension(),
]

View File

@ -1,18 +1,22 @@
from django.conf import settings
from django.db import transaction from django.db import transaction
from django.conf import settings
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
class AtomicSlugRetryMixin: class AtomicSlugRetryMixin:
"""Makes :see:LocalizedUniqueSlugField work by retrying upon violation of """Makes :see:LocalizedUniqueSlugField work by retrying upon
the UNIQUE constraint.""" violation of the UNIQUE constraint."""
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Saves this model instance to the database.""" """Saves this model instance to the database."""
max_retries = getattr(settings, "LOCALIZED_FIELDS_MAX_RETRIES", 100) max_retries = getattr(
settings,
'LOCALIZED_FIELDS_MAX_RETRIES',
100
)
if not hasattr(self, "retries"): if not hasattr(self, 'retries'):
self.retries = 0 self.retries = 0
with transaction.atomic(): with transaction.atomic():
@ -24,7 +28,7 @@ class AtomicSlugRetryMixin:
# field class... we can also not only catch exceptions # field class... we can also not only catch exceptions
# that apply to slug fields... so yea.. this is as # that apply to slug fields... so yea.. this is as
# retarded as it gets... i am sorry :( # retarded as it gets... i am sorry :(
if "slug" not in str(ex): if 'slug' not in str(ex):
raise ex raise ex
if self.retries >= max_retries: if self.retries >= max_retries:

View File

@ -10,8 +10,7 @@ class LocalizedModel(AtomicSlugRetryMixin, PostgresModel):
from LocalizedModel. However, for certain features, this is required. from LocalizedModel. However, for certain features, this is required.
It is definitely needed for :see:LocalizedUniqueSlugField, unless you It is definitely needed for :see:LocalizedUniqueSlugField, unless you
manually inherit from AtomicSlugRetryMixin. manually inherit from AtomicSlugRetryMixin."""
"""
class Meta: class Meta:
abstract = True abstract = True

View File

@ -34,11 +34,10 @@
.localized-fields-widget.tabs .localized-fields-widget.tab label { .localized-fields-widget.tabs .localized-fields-widget.tab label {
padding: 5px 10px; padding: 5px 10px;
display: inline; display: inline-block;
text-decoration: none; text-decoration: none;
color: #fff; color: #fff;
width: initial; width: initial;
cursor: pointer;
} }
.localized-fields-widget.tabs .localized-fields-widget.tab.active, .localized-fields-widget.tabs .localized-fields-widget.tab.active,

View File

@ -15,7 +15,10 @@ def get_language_codes() -> List[str]:
in your project. in your project.
""" """
return [lang_code for lang_code, _ in settings.LANGUAGES] return [
lang_code
for lang_code, _ in settings.LANGUAGES
]
def resolve_object_property(obj, path: str): def resolve_object_property(obj, path: str):
@ -35,7 +38,7 @@ def resolve_object_property(obj, path: str):
""" """
value = obj value = obj
for path_part in path.split("."): for path_part in path.split('.'):
value = getattr(value, path_part) value = getattr(value, path_part)
return value return value

View File

@ -1,18 +1,17 @@
from collections.abc import Iterable
from typing import Optional
import deprecation import deprecation
from typing import Optional
from collections.abc import Iterable
from django.conf import settings from django.conf import settings
from django.utils import translation from django.utils import translation
class LocalizedValue(dict): class LocalizedValue(dict):
"""Represents the value of a :see:LocalizedField.""" """Represents the value of a :see:LocalizedField."""
default_value = None default_value = None
def __init__(self, keys: dict = None): def __init__(self, keys: dict=None):
"""Initializes a new instance of :see:LocalizedValue. """Initializes a new instance of :see:LocalizedValue.
Arguments: Arguments:
@ -25,8 +24,9 @@ class LocalizedValue(dict):
super().__init__({}) super().__init__({})
self._interpret_value(keys) self._interpret_value(keys)
def get(self, language: str = None, default: str = None) -> str: def get(self, language: str=None, default: str=None) -> str:
"""Gets the underlying value in the specified or primary language. """Gets the underlying value in the specified or
primary language.
Arguments: Arguments:
language: language:
@ -65,12 +65,12 @@ class LocalizedValue(dict):
contained in this instance. contained in this instance.
""" """
path = "localized_fields.value.%s" % self.__class__.__name__ path = 'localized_fields.value.%s' % self.__class__.__name__
return path, [self.__dict__], {} return path, [self.__dict__], {}
def _interpret_value(self, value): def _interpret_value(self, value):
"""Interprets a value passed in the constructor as a """Interprets a value passed in the constructor as
:see:LocalizedValue. a :see:LocalizedValue.
If string: If string:
Assumes it's the default language. Assumes it's the default language.
@ -90,9 +90,6 @@ class LocalizedValue(dict):
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
self.set(lang_code, self.default_value) self.set(lang_code, self.default_value)
if callable(value):
value = value()
if isinstance(value, str): if isinstance(value, str):
self.set(settings.LANGUAGE_CODE, value) self.set(settings.LANGUAGE_CODE, value)
@ -105,54 +102,34 @@ class LocalizedValue(dict):
for val in value: for val in value:
self._interpret_value(val) self._interpret_value(val)
def translate(self, language: Optional[str] = None) -> Optional[str]: def translate(self) -> Optional[str]:
"""Gets the value in the specified language (or active language). """Gets the value in the current language or falls
back to the next language if there's no value in the
current language."""
Arguments: fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {})
language:
The language to get the value in. If not specified,
the currently active language is used.
Returns: language = translation.get_language() or settings.LANGUAGE_CODE
The value in the specified (or active) language. If no value languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:]
is available in the specified language, the value is returned languages.insert(0, language)
in one of the fallback languages.
"""
target_language = ( for lang_code in languages:
language or translation.get_language() or settings.LANGUAGE_CODE
)
fallback_config = getattr(settings, "LOCALIZED_FIELDS_FALLBACKS", {})
target_languages = fallback_config.get(
target_language, [settings.LANGUAGE_CODE]
)
for lang_code in [target_language] + target_languages:
value = self.get(lang_code) value = self.get(lang_code)
if value: if value:
return value or None return value or None
return None return None
def is_empty(self) -> bool:
"""Gets whether all the languages contain the default value."""
for lang_code, _ in settings.LANGUAGES:
if self.get(lang_code) != self.default_value:
return False
return True
def __str__(self) -> str: def __str__(self) -> str:
"""Gets the value in the current language or falls back to the next """Gets the value in the current language or falls
language if there's no value in the current language.""" back to the next language if there's no value in the
current language."""
return self.translate() or "" return self.translate() or ''
def __eq__(self, other): def __eq__(self, other):
"""Compares :paramref:self to :paramref:other for equality. """Compares :paramref:self to :paramref:other for
equality.
Returns: Returns:
True when :paramref:self is equal to :paramref:other. True when :paramref:self is equal to :paramref:other.
@ -171,7 +148,8 @@ class LocalizedValue(dict):
return True return True
def __ne__(self, other): def __ne__(self, other):
"""Compares :paramref:self to :paramerf:other for in-equality. """Compares :paramref:self to :paramerf:other for
in-equality.
Returns: Returns:
True when :paramref:self is not equal to :paramref:other. True when :paramref:self is not equal to :paramref:other.
@ -196,76 +174,38 @@ class LocalizedValue(dict):
def __repr__(self): # pragma: no cover def __repr__(self): # pragma: no cover
"""Gets a textual representation of this object.""" """Gets a textual representation of this object."""
return "%s<%s> 0x%s" % ( return '%s<%s> 0x%s' % (self.__class__.__name__,
self.__class__.__name__, self.__dict__, id(self))
self.__dict__,
id(self),
)
class LocalizedStringValue(LocalizedValue): class LocalizedStringValue(LocalizedValue):
default_value = "" default_value = ''
class LocalizedFileValue(LocalizedValue): class LocalizedFileValue(LocalizedValue):
def __getattr__(self, name: str): def __getattr__(self, name: str):
"""Proxies access to attributes to attributes of LocalizedFile.""" """Proxies access to attributes to attributes of LocalizedFile"""
value = self.get(translation.get_language()) value = self.get(translation.get_language())
if hasattr(value, name): if hasattr(value, name):
return getattr(value, name) return getattr(value, name)
raise AttributeError( raise AttributeError("'{}' object has no attribute '{}'".
"'{}' object has no attribute '{}'".format( format(self.__class__.__name__, name))
self.__class__.__name__, name
)
)
def __str__(self) -> str: def __str__(self) -> str:
"""Returns string representation of value.""" """Returns string representation of value"""
return str(super().__str__()) return str(super().__str__())
@deprecation.deprecated( @deprecation.deprecated(deprecated_in='4.6', removed_in='5.0',
deprecated_in="4.6", current_version='4.6',
removed_in="5.0", details='Use the translate() function instead.')
current_version="4.6",
details="Use the translate() function instead.",
)
def localized(self): def localized(self):
"""Returns value for current language.""" """Returns value for current language"""
return self.get(translation.get_language()) return self.get(translation.get_language())
class LocalizedBooleanValue(LocalizedValue):
def translate(self):
"""Gets the value in the current language, or in the configured fallbck
language."""
value = super().translate()
if value is None or (isinstance(value, str) and value.strip() == ""):
return None
if isinstance(value, bool):
return value
if value.lower() == "true":
return True
return False
def __bool__(self):
"""Gets the value in the current language as a boolean."""
value = self.translate()
return value
def __str__(self):
"""Returns string representation of value."""
value = self.translate()
return str(value) if value is not None else ""
class LocalizedNumericValue(LocalizedValue): class LocalizedNumericValue(LocalizedValue):
def __int__(self): def __int__(self):
"""Gets the value in the current language as an integer.""" """Gets the value in the current language as an integer."""
@ -276,13 +216,13 @@ class LocalizedNumericValue(LocalizedValue):
return int(value) return int(value)
def __str__(self) -> str: def __str__(self) -> str:
"""Returns string representation of value.""" """Returns string representation of value"""
value = self.translate() value = self.translate()
return str(value) if value is not None else "" return str(value) if value is not None else ''
def __float__(self): def __float__(self):
"""Gets the value in the current language as a float.""" """Gets the value in the current language as a float"""
value = self.translate() value = self.translate()
if value is None: if value is None:
return self.default_value return self.default_value
@ -296,26 +236,28 @@ class LocalizedIntegerValue(LocalizedNumericValue):
default_value = None default_value = None
def translate(self): def translate(self):
"""Gets the value in the current language, or in the configured fallbck """Gets the value in the current language, or
language.""" in the configured fallbck language."""
value = super().translate() value = super().translate()
if value is None or (isinstance(value, str) and value.strip() == ""): if value is None or (isinstance(value, str) and value.strip() == ''):
return None return None
return int(value) return int(value)
class LocalizedFloatValue(LocalizedNumericValue): class LocalizedFloatValue(LocalizedNumericValue):
"""All values are floats.""" """All values are floats"""
default_value = None default_value = None
def translate(self): def translate(self):
"""Gets the value in the current language, or in the configured """
fallback language.""" Gets the value in the current language, or in the configured
fallback language.
"""
value = super().translate() value = super().translate()
if value is None or (isinstance(value, str) and value.strip() == ""): if value is None or (isinstance(value, str) and value.strip() == ''):
return None return None
return float(value) return float(value)

View File

@ -2,8 +2,8 @@ import copy
from typing import List from typing import List
from django import forms
from django.conf import settings from django.conf import settings
from django import forms
from django.contrib.admin import widgets from django.contrib.admin import widgets
from .value import LocalizedValue from .value import LocalizedValue
@ -11,27 +11,27 @@ from .value import LocalizedValue
class LocalizedFieldWidget(forms.MultiWidget): class LocalizedFieldWidget(forms.MultiWidget):
"""Widget that has an input box for every language.""" """Widget that has an input box for every language."""
template_name = 'localized_fields/multiwidget.html'
template_name = "localized_fields/multiwidget.html"
widget = forms.Textarea widget = forms.Textarea
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldWidget.""" """Initializes a new instance of :see:LocalizedFieldWidget."""
initial_widgets = [copy.copy(self.widget) for _ in settings.LANGUAGES] initial_widgets = [
copy.copy(self.widget)
for _ in settings.LANGUAGES
]
super().__init__(initial_widgets, *args, **kwargs) super().__init__(initial_widgets, *args, **kwargs)
for ((lang_code, lang_name), widget) in zip( for ((lang_code, lang_name), widget) in zip(settings.LANGUAGES, self.widgets):
settings.LANGUAGES, self.widgets widget.attrs['lang'] = lang_code
):
widget.attrs["lang"] = lang_code
widget.lang_code = lang_code widget.lang_code = lang_code
widget.lang_name = lang_name widget.lang_name = lang_name
def decompress(self, value: LocalizedValue) -> List[str]: def decompress(self, value: LocalizedValue) -> List[str]:
"""Decompresses the specified value so it can be spread over the """Decompresses the specified value so
internal widgets. it can be spread over the internal widgets.
Arguments: Arguments:
value: value:
@ -61,77 +61,59 @@ class LocalizedFieldWidget(forms.MultiWidget):
if not isinstance(value, list): if not isinstance(value, list):
value = self.decompress(value) value = self.decompress(value)
final_attrs = context["widget"]["attrs"] final_attrs = context['widget']['attrs']
input_type = final_attrs.pop("type", None) input_type = final_attrs.pop('type', None)
id_ = final_attrs.get("id") id_ = final_attrs.get('id')
subwidgets = [] subwidgets = []
for i, widget in enumerate(self.widgets): for i, widget in enumerate(self.widgets):
if input_type is not None: if input_type is not None:
widget.input_type = input_type widget.input_type = input_type
widget_name = "%s_%s" % (name, i) widget_name = '%s_%s' % (name, i)
try: try:
widget_value = value[i] widget_value = value[i]
except IndexError: except IndexError:
widget_value = None widget_value = None
if id_: if id_:
widget_attrs = final_attrs.copy() widget_attrs = final_attrs.copy()
widget_attrs["id"] = "%s_%s" % (id_, i) widget_attrs['id'] = '%s_%s' % (id_, i)
else: else:
widget_attrs = final_attrs widget_attrs = final_attrs
widget_attrs = self.build_widget_attrs( widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs)
widget, widget_value, widget_attrs widget_context = widget.get_context(widget_name, widget_value, widget_attrs)['widget']
) widget_context.update(dict(
widget_context = widget.get_context( lang_code=widget.lang_code,
widget_name, widget_value, widget_attrs lang_name=widget.lang_name
)["widget"] ))
widget_context.update(
dict(lang_code=widget.lang_code, lang_name=widget.lang_name)
)
subwidgets.append(widget_context) subwidgets.append(widget_context)
context["widget"]["subwidgets"] = subwidgets context['widget']['subwidgets'] = subwidgets
return context return context
@staticmethod @staticmethod
def build_widget_attrs(widget, value, attrs): def build_widget_attrs(widget, value, attrs):
attrs = dict(attrs) # Copy attrs to avoid modifying the argument. attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
if ( if (not widget.use_required_attribute(value) or not widget.is_required) \
not widget.use_required_attribute(value) or not widget.is_required and 'required' in attrs:
) and "required" in attrs: del attrs['required']
del attrs["required"]
return attrs return attrs
class LocalizedCharFieldWidget(LocalizedFieldWidget): class LocalizedCharFieldWidget(LocalizedFieldWidget):
"""Widget that has an input box for every language.""" """Widget that has an input box for every language."""
widget = forms.TextInput widget = forms.TextInput
class LocalizedFileWidget(LocalizedFieldWidget): class LocalizedFileWidget(LocalizedFieldWidget):
"""Widget that has an file input box for every language.""" """Widget that has an file input box for every language."""
widget = forms.ClearableFileInput widget = forms.ClearableFileInput
class AdminLocalizedFieldWidget(LocalizedFieldWidget): class AdminLocalizedFieldWidget(LocalizedFieldWidget):
template_name = "localized_fields/admin/widget.html" template_name = 'localized_fields/admin/widget.html'
widget = widgets.AdminTextareaWidget widget = widgets.AdminTextareaWidget
class AdminLocalizedBooleanFieldWidget(LocalizedFieldWidget):
widget = forms.Select
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedBooleanFieldWidget."""
super().__init__(*args, **kwargs)
for widget in self.widgets:
widget.choices = [("False", False), ("True", True)]
class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget):
widget = widgets.AdminTextInputWidget widget = widgets.AdminTextInputWidget

View File

@ -1,2 +0,0 @@
[tool.black]
line-length = 80

View File

@ -1,7 +0,0 @@
[pytest]
DJANGO_SETTINGS_MODULE=settings
testpaths=tests
addopts=-m "not benchmark"
junit_family=legacy
filterwarnings=
ignore::DeprecationWarning:localized_fields.fields.autoslug_field

2
requirements/base.txt Normal file
View File

@ -0,0 +1,2 @@
django-postgres-extra==1.22
deprecation==2.0.7

10
requirements/test.txt Normal file
View File

@ -0,0 +1,10 @@
-r base.txt
django-autoslug==1.9.3
django-bleach==0.3.0
psycopg2==2.7.3.2
coverage==4.4.2
flake8==3.6.0
pep8==1.7.1
dj-database-url==0.4.2
tox==2.9.1

View File

@ -1,4 +1,3 @@
import django
import dj_database_url import dj_database_url
DEBUG = True DEBUG = True
@ -9,7 +8,7 @@ SECRET_KEY = 'this is my secret key' # NOQA
TEST_RUNNER = 'django.test.runner.DiscoverRunner' TEST_RUNNER = 'django.test.runner.DiscoverRunner'
DATABASES = { DATABASES = {
'default': dj_database_url.config(default='postgres:///localized_fields'), 'default': dj_database_url.config(default='postgres:///localized_fields')
} }
DATABASES['default']['ENGINE'] = 'psqlextra.backend' DATABASES['default']['ENGINE'] = 'psqlextra.backend'
@ -52,12 +51,6 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
] ]
# See: https://github.com/psycopg/psycopg2/issues/1293
if django.VERSION >= (3, 1):
USE_TZ = True
USE_I18N = True
TIME_ZONE = 'UTC'
# set to a lower number than the default, since # set to a lower number than the default, since
# we want the tests to be fast, default is 100 # we want the tests to be fast, default is 100
LOCALIZED_FIELDS_MAX_RETRIES = 3 LOCALIZED_FIELDS_MAX_RETRIES = 3

View File

@ -1,12 +1,8 @@
[flake8] [flake8]
ignore = E252,E501,W503 max-line-length = 120
ignore = E252, W605
exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules exclude = env,.tox,.git,config/settings,*/migrations/*,*/static/CACHE/*,docs,node_modules
[isort] [pep8]
line_length=80 max-line-length = 120
multi_line_output=3 exclude=env,.tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules
lines_between_types=1
include_trailing_comma=True
not_skip=__init__.py
known_standard_library=dataclasses
known_third_party=django_bleach,bleach,pytest

181
setup.py
View File

@ -1,5 +1,5 @@
import distutils.cmd
import os import os
import distutils.cmd
import subprocess import subprocess
from setuptools import find_packages, setup from setuptools import find_packages, setup
@ -28,168 +28,41 @@ def create_command(text, commands):
return CustomCommand return CustomCommand
with open( with open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='utf-8') as readme:
os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf-8"
) as readme:
README = readme.read() README = readme.read()
setup( setup(
name="django-localized-fields", name='django-localized-fields',
version="6.8b5", version='5.4.2',
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=['tests']),
include_package_data=True, include_package_data=True,
license="MIT License", license='MIT License',
description="Implementation of localized model fields using PostgreSQL HStore fields.", description='Implementation of localized model fields using PostgreSQL HStore fields.',
long_description=README, long_description=README,
long_description_content_type="text/markdown", url='https://github.com/SectorLabs/django-localized-fields',
url="https://github.com/SectorLabs/django-localized-fields", author='Sector Labs',
author="Sector Labs", author_email='open-source@sectorlabs.ro',
author_email="open-source@sectorlabs.ro", keywords=['django', 'localized', 'language', 'models', 'fields'],
keywords=[ install_requires=[
"django", 'django-postgres-extra>=1.22,<2.0',
"localized", 'Django>=1.11',
"language", 'deprecation==2.0.7'
"models",
"fields",
"postgres",
"hstore",
"i18n",
], ],
classifiers=[ classifiers=[
"Environment :: Web Environment", 'Environment :: Web Environment',
"Framework :: Django", 'Framework :: Django',
"Intended Audience :: Developers", 'Intended Audience :: Developers',
"License :: OSI Approved :: MIT License", 'License :: OSI Approved :: MIT License',
"Operating System :: OS Independent", 'Operating System :: OS Independent',
"Programming Language :: Python", 'Programming Language :: Python',
"Programming Language :: Python :: 3.6", 'Programming Language :: Python :: 3.5',
"Programming Language :: Python :: 3.7", 'Topic :: Internet :: WWW/HTTP',
"Programming Language :: Python :: 3.8", 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
], ],
python_requires=">=3.6",
install_requires=[
"Django>=2.0",
"django-postgres-extra>=2.0,<3.0",
"deprecation>=2.0.7",
],
extras_require={
':python_version <= "3.6"': ["dataclasses"],
"docs": ["Sphinx==2.2.0", "sphinx-rtd-theme==0.4.3"],
"test": [
"tox==3.28.0",
"pytest==7.0.1",
"pytest-django==4.5.2",
"pytest-cov==2.12.1",
"dj-database-url==0.5.0",
"django-autoslug==1.9.9",
"django-bleach==0.9.0",
"psycopg2==2.9.8",
],
"analysis": [
"black==22.3.0",
"flake8==3.7.7",
"autoflake==1.3",
"autopep8==1.4.4",
"isort==4.3.20",
"sl-docformatter==1.4",
"click==8.0.2",
],
},
cmdclass={ cmdclass={
"lint": create_command( 'lint': create_command(
"Lints the code", 'Lints the code',
[["flake8", "setup.py", "localized_fields", "tests"]], [['flake8', 'setup.py', 'localized_fields', 'tests']],
),
"lint_fix": create_command(
"Lints the code",
[
[
"autoflake",
"--remove-all-unused-imports",
"-i",
"-r",
"setup.py",
"localized_fields",
"tests",
],
[
"autopep8",
"-i",
"-r",
"setup.py",
"localized_fields",
"tests",
],
],
),
"format": create_command(
"Formats the code",
[["black", "setup.py", "localized_fields", "tests"]],
),
"format_verify": create_command(
"Checks if the code is auto-formatted",
[["black", "--check", "setup.py", "localized_fields", "tests"]],
),
"format_docstrings": create_command(
"Auto-formats doc strings", [["docformatter", "-r", "-i", "."]]
),
"format_docstrings_verify": create_command(
"Verifies that doc strings are properly formatted",
[["docformatter", "-r", "-c", "."]],
),
"sort_imports": create_command(
"Automatically sorts imports",
[
["isort", "setup.py"],
["isort", "-rc", "localized_fields"],
["isort", "-rc", "tests"],
],
),
"sort_imports_verify": create_command(
"Verifies all imports are properly sorted.",
[
["isort", "-c", "setup.py"],
["isort", "-c", "-rc", "localized_fields"],
["isort", "-c", "-rc", "tests"],
],
),
"fix": create_command(
"Automatically format code and fix linting errors",
[
["python", "setup.py", "format"],
["python", "setup.py", "format_docstrings"],
["python", "setup.py", "sort_imports"],
["python", "setup.py", "lint_fix"],
],
),
"verify": create_command(
"Verifies whether the code is auto-formatted and has no linting errors",
[
["python", "setup.py", "format_verify"],
["python", "setup.py", "format_docstrings_verify"],
["python", "setup.py", "sort_imports_verify"],
["python", "setup.py", "lint"],
],
),
"test": create_command(
"Runs all the tests",
[
[
"pytest",
"--cov=localized_fields",
"--cov-report=term",
"--cov-report=xml:reports/xml",
"--cov-report=html:reports/html",
"--junitxml=reports/junit/tests.xml",
"--reuse-db",
]
],
), ),
}, },
) )

View File

@ -2,11 +2,12 @@ from django.conf import settings
def get_init_values() -> dict: def get_init_values() -> dict:
"""Gets a test dictionary containing a key for every language.""" """Gets a test dictionary containing a key
for every language."""
keys = {} keys = {}
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
keys[lang_code] = "value in %s" % lang_name keys[lang_code] = 'value in %s' % lang_name
return keys return keys

View File

@ -1,20 +1,20 @@
import uuid import uuid
from django.contrib.postgres.operations import HStoreExtension
from django.db import connection, migrations from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
from django.contrib.postgres.operations import HStoreExtension
from localized_fields.models import LocalizedModel from localized_fields.models import LocalizedModel
def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}):
name = str(uuid.uuid4()).replace("-", "")[:8] name = str(uuid.uuid4()).replace('-', '')[:8]
attributes = { attributes = {
"app_label": "tests", 'app_label': 'tests',
"__module__": __name__, '__module__': __name__,
"__name__": name, '__name__': name,
"Meta": type("Meta", (object,), meta_options), 'Meta': type('Meta', (object,), meta_options)
} }
if fields: if fields:
@ -30,6 +30,7 @@ def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}):
model = define_fake_model(fields, model_base, meta_options) model = define_fake_model(fields, model_base, meta_options)
class TestProject: class TestProject:
def clone(self, *_args, **_kwargs): def clone(self, *_args, **_kwargs):
return self return self
@ -43,8 +44,7 @@ def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}):
with connection.schema_editor() as schema_editor: with connection.schema_editor() as schema_editor:
migration_executor = MigrationExecutor(schema_editor.connection) migration_executor = MigrationExecutor(schema_editor.connection)
migration_executor.apply_migration( migration_executor.apply_migration(
TestProject(), TestMigration("eh", "postgres_extra") TestProject(), TestMigration('eh', 'postgres_extra'))
)
schema_editor.create_model(model) schema_editor.create_model(model)

View File

@ -4,8 +4,9 @@ from django.contrib.admin.checks import check_admin_app
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from localized_fields.admin import LocalizedFieldsAdminMixin
from localized_fields.fields import LocalizedField from localized_fields.fields import LocalizedField
from localized_fields.admin import LocalizedFieldsAdminMixin
from tests.fake_model import get_fake_model from tests.fake_model import get_fake_model
@ -21,13 +22,16 @@ class LocalizedFieldsAdminMixinTestCase(TestCase):
super(LocalizedFieldsAdminMixinTestCase, cls).setUpClass() super(LocalizedFieldsAdminMixinTestCase, cls).setUpClass()
cls.TestRelModel = get_fake_model({"description": LocalizedField()}) cls.TestRelModel = get_fake_model(
{
'description': LocalizedField()
}
)
cls.TestModel = get_fake_model( cls.TestModel = get_fake_model(
{ {
"title": LocalizedField(), 'title': LocalizedField(),
"rel": models.ForeignKey( 'rel': models.ForeignKey(cls.TestRelModel,
cls.TestRelModel, on_delete=models.CASCADE on_delete=models.CASCADE)
),
} }
) )
@ -39,8 +43,8 @@ class LocalizedFieldsAdminMixinTestCase(TestCase):
@classmethod @classmethod
def test_model_admin(cls): def test_model_admin(cls):
"""Tests whether :see:LocalizedFieldsAdminMixin mixin are works with """Tests whether :see:LocalizedFieldsAdminMixin
admin.ModelAdmin.""" mixin are works with admin.ModelAdmin"""
@admin.register(cls.TestModel) @admin.register(cls.TestModel)
class TestModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin): class TestModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin):
@ -50,32 +54,34 @@ class LocalizedFieldsAdminMixinTestCase(TestCase):
@classmethod @classmethod
def test_stackedmodel_admin(cls): def test_stackedmodel_admin(cls):
"""Tests whether :see:LocalizedFieldsAdminMixin mixin are works with """Tests whether :see:LocalizedFieldsAdminMixin mixin are works
admin.StackedInline.""" with admin.StackedInline"""
class TestModelStackedInline( class TestModelStackedInline(LocalizedFieldsAdminMixin,
LocalizedFieldsAdminMixin, admin.StackedInline admin.StackedInline):
):
model = cls.TestModel model = cls.TestModel
@admin.register(cls.TestRelModel) @admin.register(cls.TestRelModel)
class TestRelModelAdmin(admin.ModelAdmin): class TestRelModelAdmin(admin.ModelAdmin):
inlines = [TestModelStackedInline] inlines = [
TestModelStackedInline,
]
assert len(check_admin_app(apps.get_app_configs())) == 0 assert len(check_admin_app(apps.get_app_configs())) == 0
@classmethod @classmethod
def test_tabularmodel_admin(cls): def test_tabularmodel_admin(cls):
"""Tests whether :see:LocalizedFieldsAdminMixin mixin are works with """Tests whether :see:LocalizedFieldsAdminMixin mixin are works
admin.TabularInline.""" with admin.TabularInline"""
class TestModelTabularInline( class TestModelTabularInline(LocalizedFieldsAdminMixin,
LocalizedFieldsAdminMixin, admin.TabularInline admin.TabularInline):
):
model = cls.TestModel model = cls.TestModel
@admin.register(cls.TestRelModel) @admin.register(cls.TestRelModel)
class TestRelModelAdmin(admin.ModelAdmin): class TestRelModelAdmin(admin.ModelAdmin):
inlines = [TestModelTabularInline] inlines = [
TestModelTabularInline,
]
assert len(check_admin_app(apps.get_app_configs())) == 0 assert len(check_admin_app(apps.get_app_configs())) == 0

View File

@ -1,30 +1,18 @@
"""isort:skip_file.""" import bleach
import sys
import html
import pytest
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django_bleach.utils import get_bleach_default_options
from localized_fields.fields import LocalizedBleachField from localized_fields.fields import LocalizedBleachField
from localized_fields.value import LocalizedValue from localized_fields.value import LocalizedValue
try:
import bleach
from django_bleach.utils import get_bleach_default_options class TestModel:
except ImportError:
if sys.version_info >= (3, 9):
pytest.skip("feature not ready for python 3.9", allow_module_level=True)
class ModelTest:
"""Used to declare a bleach-able field on.""" """Used to declare a bleach-able field on."""
def __init__(self, value): def __init__(self, value):
"""Initializes a new instance of :see:ModelTest. """Initializes a new instance of :see:TestModel.
Arguments: Arguments:
The value to initialize with. The value to initialize with.
@ -37,8 +25,8 @@ class LocalizedBleachFieldTestCase(TestCase):
"""Tests the :see:LocalizedBleachField class.""" """Tests the :see:LocalizedBleachField class."""
def test_pre_save(self): def test_pre_save(self):
"""Tests whether the :see:pre_save function bleaches all values in a """Tests whether the :see:pre_save function
:see:LocalizedValue.""" bleaches all values in a :see:LocalizedValue."""
value = self._get_test_value() value = self._get_test_value()
model, field = self._get_test_model(value) model, field = self._get_test_model(value)
@ -47,8 +35,8 @@ class LocalizedBleachFieldTestCase(TestCase):
self._validate(value, bleached_value) self._validate(value, bleached_value)
def test_pre_save_none(self): def test_pre_save_none(self):
"""Tests whether the :see:pre_save function works properly when """Tests whether the :see:pre_save function
specifying :see:None.""" works properly when specifying :see:None."""
model, field = self._get_test_model(None) model, field = self._get_test_model(None)
@ -56,8 +44,9 @@ class LocalizedBleachFieldTestCase(TestCase):
assert not bleached_value assert not bleached_value
def test_pre_save_none_values(self): def test_pre_save_none_values(self):
"""Tests whether the :see:pre_save function works properly when one of """Tests whether the :see:pre_save function
the languages has no text and is None.""" works properly when one of the languages has
no text and is None."""
value = self._get_test_value() value = self._get_test_value()
value.set(settings.LANGUAGE_CODE, None) value.set(settings.LANGUAGE_CODE, None)
@ -67,25 +56,16 @@ class LocalizedBleachFieldTestCase(TestCase):
bleached_value = field.pre_save(model, False) bleached_value = field.pre_save(model, False)
self._validate(value, bleached_value) self._validate(value, bleached_value)
def test_pre_save_do_not_escape(self):
"""Tests whether the :see:pre_save function works properly when field
escape argument is set to False."""
value = self._get_test_value()
model, field = self._get_test_model(value, escape=False)
bleached_value = field.pre_save(model, False)
self._validate(value, bleached_value, False)
@staticmethod @staticmethod
def _get_test_model(value, escape=True): def _get_test_model(value):
"""Gets a test model and an artificially constructed """Gets a test model and a artifically
:see:LocalizedBleachField instance to test with.""" constructed :see:LocalizedBleachField
instance to test with."""
model = ModelTest(value) model = TestModel(value)
field = LocalizedBleachField(escape=escape) field = LocalizedBleachField()
field.attname = "value" field.attname = 'value'
return model, field return model, field
@staticmethod @staticmethod
@ -95,14 +75,14 @@ class LocalizedBleachFieldTestCase(TestCase):
value = LocalizedValue() value = LocalizedValue()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
value.set(lang_code, "<script>%s</script>" % lang_name) value.set(lang_code, '<script>%s</script>' % lang_name)
return value return value
@staticmethod @staticmethod
def _validate(non_bleached_value, bleached_value, escaped_value=True): def _validate(non_bleached_value, bleached_value):
"""Validates whether the specified non-bleached value ended up being """Validates whether the specified non-bleached
correctly bleached. value ended up being correctly bleached.
Arguments: Arguments:
non_bleached_value: non_bleached_value:
@ -111,20 +91,15 @@ class LocalizedBleachFieldTestCase(TestCase):
bleached_value: bleached_value:
The value after bleaching. The value after bleaching.
""" """
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
if not non_bleached_value.get(lang_code): if not non_bleached_value.get(lang_code):
assert not bleached_value.get(lang_code) assert not bleached_value.get(lang_code)
continue continue
cleaned_value = bleach.clean( expected_value = bleach.clean(
non_bleached_value.get(lang_code) non_bleached_value.get(lang_code),
if escaped_value get_bleach_default_options()
else html.unescape(non_bleached_value.get(lang_code)),
get_bleach_default_options(),
)
expected_value = (
cleaned_value if escaped_value else html.unescape(cleaned_value)
) )
assert bleached_value.get(lang_code) == expected_value assert bleached_value.get(lang_code) == expected_value

View File

@ -1,211 +0,0 @@
from django.conf import settings
from django.db import connection
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import translation
from localized_fields.fields import LocalizedBooleanField
from localized_fields.value import LocalizedBooleanValue
from .fake_model import get_fake_model
class LocalizedBooleanFieldTestCase(TestCase):
"""Tests whether the :see:LocalizedBooleanField and
:see:LocalizedIntegerValue works properly."""
TestModel = None
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.TestModel = get_fake_model({"translated": LocalizedBooleanField()})
def test_basic(self):
"""Tests the basics of storing boolean values."""
obj = self.TestModel()
for lang_code, _ in settings.LANGUAGES:
obj.translated.set(lang_code, False)
obj.save()
obj = self.TestModel.objects.all().first()
for lang_code, _ in settings.LANGUAGES:
assert obj.translated.get(lang_code) is False
def test_primary_language_required(self):
"""Tests whether the primary language is required by default and all
other languages are optional."""
# not filling in anything should raise IntegrityError,
# the primary language is required
with self.assertRaises(IntegrityError):
obj = self.TestModel()
obj.save()
# when filling all other languages besides the primary language
# should still raise an error because the primary is always required
with self.assertRaises(IntegrityError):
obj = self.TestModel()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
continue
obj.translated.set(lang_code, True)
obj.save()
def test_default_value_none(self):
"""Tests whether the default value for optional languages is
NoneType."""
obj = self.TestModel()
obj.translated.set(settings.LANGUAGE_CODE, True)
obj.save()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
continue
assert obj.translated.get(lang_code) is None
def test_translate(self):
"""Tests whether casting the value to a boolean results in the value
being returned in the currently active language as a boolean."""
obj = self.TestModel()
for lang_code, _ in settings.LANGUAGES:
obj.translated.set(lang_code, True)
obj.save()
obj.refresh_from_db()
for lang_code, _ in settings.LANGUAGES:
with translation.override(lang_code):
assert bool(obj.translated) is True
assert obj.translated.translate() is True
def test_translate_primary_fallback(self):
"""Tests whether casting the value to a boolean results in the value
being returned in the active language and falls back to the primary
language if there is no value in that language."""
obj = self.TestModel()
obj.translated.set(settings.LANGUAGE_CODE, True)
secondary_language = settings.LANGUAGES[-1][0]
assert obj.translated.get(secondary_language) is None
with translation.override(secondary_language):
assert obj.translated.translate() is True
assert bool(obj.translated) is True
def test_get_default_value(self):
"""Tests whether getting the value in a specific language properly
returns the specified default in case it is not available."""
obj = self.TestModel()
obj.translated.set(settings.LANGUAGE_CODE, True)
secondary_language = settings.LANGUAGES[-1][0]
assert obj.translated.get(secondary_language) is None
assert obj.translated.get(secondary_language, False) is False
def test_completely_optional(self):
"""Tests whether having all languages optional works properly."""
model = get_fake_model(
{
"translated": LocalizedBooleanField(
null=True, required=[], blank=True
)
}
)
obj = model()
obj.save()
for lang_code, _ in settings.LANGUAGES:
assert getattr(obj.translated, lang_code) is None
def test_store_string(self):
"""Tests whether the field properly raises an error when trying to
store a non-boolean."""
for lang_code, _ in settings.LANGUAGES:
obj = self.TestModel()
with self.assertRaises(IntegrityError):
obj.translated.set(lang_code, "haha")
obj.save()
def test_none_if_illegal_value_stored(self):
"""Tests whether None is returned for a language if the value stored in
the database is not a boolean."""
obj = self.TestModel()
obj.translated.set(settings.LANGUAGE_CODE, False)
obj.save()
with connection.cursor() as cursor:
table_name = self.TestModel._meta.db_table
cursor.execute("update %s set translated = 'en=>haha'" % table_name)
with self.assertRaises(ValueError):
obj.refresh_from_db()
def test_default_value(self):
"""Tests whether a default is properly set when specified."""
model = get_fake_model(
{
"translated": LocalizedBooleanField(
default={settings.LANGUAGE_CODE: True}
)
}
)
obj = model.objects.create()
assert obj.translated.get(settings.LANGUAGE_CODE) is True
obj = model()
for lang_code, _ in settings.LANGUAGES:
obj.translated.set(lang_code, None)
obj.save()
for lang_code, _ in settings.LANGUAGES:
if lang_code == settings.LANGUAGE_CODE:
assert obj.translated.get(lang_code) is True
else:
assert obj.translated.get(lang_code) is None
def test_default_value_update(self):
"""Tests whether a default is properly set when specified during
updates."""
model = get_fake_model(
{
"translated": LocalizedBooleanField(
default={settings.LANGUAGE_CODE: True}, null=True
)
}
)
obj = model.objects.create(
translated=LocalizedBooleanValue({settings.LANGUAGE_CODE: False})
)
assert obj.translated.get(settings.LANGUAGE_CODE) is False
model.objects.update(
translated=LocalizedBooleanValue({settings.LANGUAGE_CODE: None})
)
obj.refresh_from_db()
assert obj.translated.get(settings.LANGUAGE_CODE) is True
def test_callable_default_value(self):
output = {"en": True}
def func():
return output
model = get_fake_model({"test": LocalizedBooleanField(default=func)})
obj = model.objects.create()
assert obj.test["en"] == output["en"]

View File

@ -7,34 +7,26 @@ from .fake_model import get_fake_model
class LocalizedBulkTestCase(TestCase): class LocalizedBulkTestCase(TestCase):
"""Tests bulk operations with data structures provided by the django- """Tests bulk operations with data structures provided
localized-fields library.""" by the django-localized-fields library."""
@staticmethod @staticmethod
def test_localized_bulk_insert(): def test_localized_bulk_insert():
"""Tests whether bulk inserts work properly when using a """Tests whether bulk inserts work properly when using
:see:LocalizedUniqueSlugField in the model.""" a :see:LocalizedUniqueSlugField in the model."""
model = get_fake_model( model = get_fake_model(
{ {
"name": LocalizedField(), 'name': LocalizedField(),
"slug": LocalizedUniqueSlugField( 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True),
populate_from="name", include_time=True 'score': models.IntegerField()
),
"score": models.IntegerField(),
} }
) )
to_create = [ to_create = [
model( model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1),
name={"en": "english name 1", "ro": "romanian name 1"}, score=1 model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2),
), model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3)
model(
name={"en": "english name 2", "ro": "romanian name 2"}, score=2
),
model(
name={"en": "english name 3", "ro": "romanian name 3"}, score=3
),
] ]
model.objects.bulk_create(to_create) model.objects.bulk_create(to_create)
@ -42,7 +34,9 @@ class LocalizedBulkTestCase(TestCase):
for obj in to_create: for obj in to_create:
obj_db = model.objects.filter( obj_db = model.objects.filter(
name__en=obj.name.en, name__ro=obj.name.ro, score=obj.score name__en=obj.name.en,
name__ro=obj.name.ro,
score=obj.score
).first() ).first()
assert obj_db assert obj_db

View File

@ -1,12 +1,12 @@
from django.test import TestCase
from django.db import models
from django.utils import translation
from django.conf import settings from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.aggregates import ArrayAgg
from django.db import models
from django.test import TestCase
from django.utils import translation
from localized_fields.expressions import LocalizedRef
from localized_fields.fields import LocalizedField from localized_fields.fields import LocalizedField
from localized_fields.value import LocalizedValue from localized_fields.value import LocalizedValue
from localized_fields.expressions import LocalizedRef
from .fake_model import get_fake_model from .fake_model import get_fake_model
@ -24,17 +24,15 @@ class LocalizedExpressionsTestCase(TestCase):
super(LocalizedExpressionsTestCase, cls).setUpClass() super(LocalizedExpressionsTestCase, cls).setUpClass()
cls.TestModel1 = get_fake_model( cls.TestModel1 = get_fake_model(
{"name": models.CharField(null=False, blank=False, max_length=255)} {
'name': models.CharField(null=False, blank=False, max_length=255),
}
) )
cls.TestModel2 = get_fake_model( cls.TestModel2 = get_fake_model(
{ {
"text": LocalizedField(), 'text': LocalizedField(),
"other": models.ForeignKey( 'other': models.ForeignKey(cls.TestModel1, related_name='features', on_delete=models.CASCADE)
cls.TestModel1,
related_name="features",
on_delete=models.CASCADE,
),
} }
) )
@ -42,28 +40,24 @@ class LocalizedExpressionsTestCase(TestCase):
def test_localized_ref(cls): def test_localized_ref(cls):
"""Tests whether the :see:LocalizedRef expression properly works.""" """Tests whether the :see:LocalizedRef expression properly works."""
obj = cls.TestModel1.objects.create(name="bla bla") obj = cls.TestModel1.objects.create(name='bla bla')
for i in range(0, 10): for i in range(0, 10):
cls.TestModel2.objects.create( cls.TestModel2.objects.create(
text=LocalizedValue( text=LocalizedValue(dict(en='text_%d_en' % i, ro='text_%d_ro' % i, nl='text_%d_nl' % i)),
dict( other=obj
en="text_%d_en" % i,
ro="text_%d_ro" % i,
nl="text_%d_nl" % i,
)
),
other=obj,
) )
def create_queryset(ref): def create_queryset(ref):
return cls.TestModel1.objects.annotate(mytexts=ref).values_list( return (
"mytexts", flat=True cls.TestModel1.objects
.annotate(mytexts=ref)
.values_list('mytexts', flat=True)
) )
# assert that it properly selects the currently active language # assert that it properly selects the currently active language
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
translation.activate(lang_code) translation.activate(lang_code)
queryset = create_queryset(LocalizedRef("features__text")) queryset = create_queryset(LocalizedRef('features__text'))
for index, value in enumerate(queryset): for index, value in enumerate(queryset):
assert translation.get_language() in value assert translation.get_language() in value
@ -72,21 +66,19 @@ class LocalizedExpressionsTestCase(TestCase):
# ensure that the default language is used in case no # ensure that the default language is used in case no
# language is active at all # language is active at all
translation.deactivate_all() translation.deactivate_all()
queryset = create_queryset(LocalizedRef("features__text")) queryset = create_queryset(LocalizedRef('features__text'))
for index, value in enumerate(queryset): for index, value in enumerate(queryset):
assert settings.LANGUAGE_CODE in value assert settings.LANGUAGE_CODE in value
assert str(index) in value assert str(index) in value
# ensures that overriding the language works properly # ensures that overriding the language works properly
queryset = create_queryset(LocalizedRef("features__text", "ro")) queryset = create_queryset(LocalizedRef('features__text', 'ro'))
for index, value in enumerate(queryset): for index, value in enumerate(queryset):
assert "ro" in value assert 'ro' in value
assert str(index) in value assert str(index) in value
# ensures that using this in combination with ArrayAgg works properly # ensures that using this in combination with ArrayAgg works properly
queryset = create_queryset( queryset = create_queryset(ArrayAgg(LocalizedRef('features__text', 'ro'))).first()
ArrayAgg(LocalizedRef("features__text", "ro"))
).first()
assert isinstance(queryset, list) assert isinstance(queryset, list)
for value in queryset: for value in queryset:
assert "ro" in value assert 'ro' in value

View File

@ -18,8 +18,8 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_init(): def test_init():
"""Tests whether the :see:__init__ function correctly handles """Tests whether the :see:__init__ function
parameters.""" correctly handles parameters"""
field = LocalizedField(blank=True) field = LocalizedField(blank=True)
assert field.required == [] assert field.required == []
@ -28,17 +28,16 @@ class LocalizedFieldTestCase(TestCase):
assert field.required == [settings.LANGUAGE_CODE] assert field.required == [settings.LANGUAGE_CODE]
field = LocalizedField(required=True) field = LocalizedField(required=True)
assert field.required == [ assert field.required == [lang_code for lang_code, _ in
lang_code for lang_code, _ in settings.LANGUAGES settings.LANGUAGES]
]
field = LocalizedField(required=False) field = LocalizedField(required=False)
assert field.required == [] assert field.required == []
@staticmethod @staticmethod
def test_from_db_value(): def test_from_db_value():
"""Tests whether the :see:from_db_value function produces the expected """Tests whether the :see:from_db_value function
:see:LocalizedValue.""" produces the expected :see:LocalizedValue."""
input_data = get_init_values() input_data = get_init_values()
localized_value = LocalizedField().from_db_value(input_data) localized_value = LocalizedField().from_db_value(input_data)
@ -48,8 +47,8 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_from_db_value_none(): def test_from_db_value_none():
"""Tests whether the :see:from_db_value function correctly handles None """Tests whether the :see:from_db_value function
values.""" correctly handles None values."""
localized_value = LocalizedField().from_db_value(None) localized_value = LocalizedField().from_db_value(None)
@ -57,8 +56,9 @@ class LocalizedFieldTestCase(TestCase):
assert localized_value.get(lang_code) is None assert localized_value.get(lang_code) is None
def test_from_db_value_none_return_none(self): def test_from_db_value_none_return_none(self):
"""Tests whether the :see:from_db_value function correctly handles None """Tests whether the :see:from_db_value function
values when LOCALIZED_FIELDS_EXPERIMENTAL is set to True.""" correctly handles None values when LOCALIZED_FIELDS_EXPERIMENTAL
is set to True."""
with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True): with self.settings(LOCALIZED_FIELDS_EXPERIMENTAL=True):
localized_value = LocalizedField.from_db_value(None) localized_value = LocalizedField.from_db_value(None)
@ -67,8 +67,8 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_to_python(): def test_to_python():
"""Tests whether the :see:to_python function produces the expected """Tests whether the :see:to_python function
:see:LocalizedValue.""" produces the expected :see:LocalizedValue."""
input_data = get_init_values() input_data = get_init_values()
localized_value = LocalizedField().to_python(input_data) localized_value = LocalizedField().to_python(input_data)
@ -78,16 +78,17 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_to_python_non_json(): def test_to_python_non_json():
"""Tests whether the :see:to_python function properly handles a string """Tests whether the :see:to_python function
that is not JSON.""" properly handles a string that is not JSON."""
localized_value = LocalizedField().to_python("my value") localized_value = LocalizedField().to_python('my value')
assert localized_value.get() == "my value" assert localized_value.get() == 'my value'
@staticmethod @staticmethod
def test_to_python_none(): def test_to_python_none():
"""Tests whether the :see:to_python function produces the expected """Tests whether the :see:to_python function
:see:LocalizedValue instance when it is passes None.""" produces the expected :see:LocalizedValue
instance when it is passes None."""
localized_value = LocalizedField().to_python(None) localized_value = LocalizedField().to_python(None)
assert localized_value assert localized_value
@ -97,8 +98,9 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_to_python_non_dict(): def test_to_python_non_dict():
"""Tests whether the :see:to_python function produces the expected """Tests whether the :see:to_python function produces
:see:LocalizedValue when it is passed a non-dictionary value.""" the expected :see:LocalizedValue when it is
passed a non-dictionary value."""
localized_value = LocalizedField().to_python(list()) localized_value = LocalizedField().to_python(list())
assert localized_value assert localized_value
@ -108,8 +110,9 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_to_python_str(): def test_to_python_str():
"""Tests whether the :see:to_python function produces the expected """Tests whether the :see:to_python function produces
:see:LocalizedValue when it is passed serialized string value.""" the expected :see:LocalizedValue when it is
passed serialized string value."""
serialized_str = json.dumps(get_init_values()) serialized_str = json.dumps(get_init_values())
localized_value = LocalizedField().to_python(serialized_str) localized_value = LocalizedField().to_python(serialized_str)
@ -121,8 +124,8 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_get_prep_value(): def test_get_prep_value():
"""Tests whether the :see:get_prep_value function produces the expected """"Tests whether the :see:get_prep_value function
dictionary.""" produces the expected dictionary."""
input_data = get_init_values() input_data = get_init_values()
localized_value = LocalizedValue(input_data) localized_value = LocalizedValue(input_data)
@ -135,24 +138,25 @@ class LocalizedFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_get_prep_value_none(): def test_get_prep_value_none():
"""Tests whether the :see:get_prep_value function produces the expected """Tests whether the :see:get_prep_value function
output when it is passed None.""" produces the expected output when it is passed None."""
output_data = LocalizedField().get_prep_value(None) output_data = LocalizedField().get_prep_value(None)
assert not output_data assert not output_data
@staticmethod @staticmethod
def test_get_prep_value_no_localized_value(): def test_get_prep_value_no_localized_value():
"""Tests whether the :see:get_prep_value function produces the expected """Tests whether the :see:get_prep_value function
output when it is passed a non-LocalizedValue value.""" produces the expected output when it is passed a
non-LocalizedValue value."""
output_data = LocalizedField().get_prep_value(["huh"]) output_data = LocalizedField().get_prep_value(['huh'])
assert not output_data assert not output_data
def test_get_prep_value_clean(self): def test_get_prep_value_clean(self):
"""Tests whether the :see:get_prep_value produces None as the output """Tests whether the :see:get_prep_value produces
when it is passed an empty, but valid LocalizedValue value but, only None as the output when it is passed an empty, but
when null=True.""" valid LocalizedValue value but, only when null=True."""
localized_value = LocalizedValue() localized_value = LocalizedValue()
@ -161,14 +165,17 @@ class LocalizedFieldTestCase(TestCase):
assert not LocalizedField(null=True).get_prep_value(localized_value) assert not LocalizedField(null=True).get_prep_value(localized_value)
assert not LocalizedField().clean(None) assert not LocalizedField().clean(None)
assert not LocalizedField().clean(["huh"]) assert not LocalizedField().clean(['huh'])
@staticmethod @staticmethod
def test_formfield(): def test_formfield():
"""Tests whether the :see:formfield function correctly returns a valid """Tests whether the :see:formfield function
form.""" correctly returns a valid form."""
assert isinstance(LocalizedField().formfield(), LocalizedFieldForm) assert isinstance(
LocalizedField().formfield(),
LocalizedFieldForm
)
# case optional filling # case optional filling
field = LocalizedField(blank=True, required=[]) field = LocalizedField(blank=True, required=[])
@ -183,7 +190,7 @@ class LocalizedFieldTestCase(TestCase):
assert not field.required assert not field.required
# case required for specific languages # case required for specific languages
required_langs = ["ro", "nl"] required_langs = ['ro', 'nl']
field = LocalizedField(blank=False, required=required_langs) field = LocalizedField(blank=False, required=required_langs)
assert field.formfield().required assert field.formfield().required
for field in field.formfield().fields: for field in field.formfield().fields:
@ -199,31 +206,31 @@ class LocalizedFieldTestCase(TestCase):
assert field.required assert field.required
def test_descriptor_user_defined_primary_key(self): def test_descriptor_user_defined_primary_key(self):
"""Tests that descriptor works even when primary key is user """Tests that descriptor works even when primary key is user defined."""
defined.""" model = get_fake_model(dict(
model = get_fake_model( slug=models.SlugField(primary_key=True),
dict( title=LocalizedField()
slug=models.SlugField(primary_key=True), title=LocalizedField() ))
)
)
obj = model.objects.create(slug="test", title="test") obj = model.objects.create(slug='test', title='test')
assert obj.title == "test" assert obj.title == 'test'
def test_required_all(self): def test_required_all(self):
"""Tests whether passing required=True properly validates that all """Tests whether passing required=True properly validates
languages are filled in.""" that all languages are filled in."""
model = get_fake_model(dict(title=LocalizedField(required=True))) model = get_fake_model(dict(
title=LocalizedField(required=True)
))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(ro="romanian", nl="dutch")) model.objects.create(title=dict(ro='romanian', nl='dutch'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(nl="dutch")) model.objects.create(title=dict(nl='dutch'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(random="random")) model.objects.create(title=dict(random='random'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict()) model.objects.create(title=dict())
@ -232,27 +239,28 @@ class LocalizedFieldTestCase(TestCase):
model.objects.create(title=None) model.objects.create(title=None)
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title="") model.objects.create(title='')
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=" ") model.objects.create(title=' ')
def test_required_some(self): def test_required_some(self):
"""Tests whether passing an array to required, properly validates """Tests whether passing an array to required,
whether the specified languages are marked as required.""" properly validates whether the specified languages
are marked as required."""
model = get_fake_model( model = get_fake_model(dict(
dict(title=LocalizedField(required=["nl", "ro"])) title=LocalizedField(required=['nl', 'ro'])
) ))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(ro="romanian", nl="dutch")) model.objects.create(title=dict(ro='romanian', nl='dutch'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(nl="dutch")) model.objects.create(title=dict(nl='dutch'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict(random="random")) model.objects.create(title=dict(random='random'))
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=dict()) model.objects.create(title=dict())
@ -261,7 +269,7 @@ class LocalizedFieldTestCase(TestCase):
model.objects.create(title=None) model.objects.create(title=None)
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title="") model.objects.create(title='')
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
model.objects.create(title=" ") model.objects.create(title=' ')

View File

@ -1,22 +1,22 @@
import json
import os import os
import pickle
import shutil import shutil
import tempfile as sys_tempfile import tempfile as sys_tempfile
import pickle
import json
from django import forms from django import forms
from django.core.files import temp as tempfile
from django.core.files.base import ContentFile, File
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.core.files.base import File, ContentFile
from django.core.files import temp as tempfile
from localized_fields.fields import LocalizedFileField from localized_fields.fields import LocalizedFileField
from localized_fields.value import LocalizedValue
from localized_fields.fields.file_field import LocalizedFieldFile from localized_fields.fields.file_field import LocalizedFieldFile
from localized_fields.forms import LocalizedFileFieldForm from localized_fields.forms import LocalizedFileFieldForm
from localized_fields.value import LocalizedFileValue, LocalizedValue from localized_fields.value import LocalizedFileValue
from localized_fields.widgets import LocalizedFileWidget from localized_fields.widgets import LocalizedFileWidget
from .fake_model import get_fake_model from .fake_model import get_fake_model
MEDIA_ROOT = sys_tempfile.mkdtemp() MEDIA_ROOT = sys_tempfile.mkdtemp()
@ -30,7 +30,11 @@ class LocalizedFileFieldTestCase(TestCase):
super().setUpClass() super().setUpClass()
cls.FileFieldModel = get_fake_model({"file": LocalizedFileField()}) cls.FileFieldModel = get_fake_model(
{
'file': LocalizedFileField(),
}
)
if not os.path.isdir(MEDIA_ROOT): if not os.path.isdir(MEDIA_ROOT):
os.makedirs(MEDIA_ROOT) os.makedirs(MEDIA_ROOT)
@ -41,49 +45,48 @@ class LocalizedFileFieldTestCase(TestCase):
@classmethod @classmethod
def test_assign(cls): def test_assign(cls):
"""Tests whether the :see:LocalizedFileValueDescriptor works """Tests whether the :see:LocalizedFileValueDescriptor works properly"""
properly."""
temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT) temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT)
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
instance.file = {"en": temp_file.name} instance.file = {'en': temp_file.name}
assert isinstance(instance.file.en, LocalizedFieldFile) assert isinstance(instance.file.en, LocalizedFieldFile)
assert instance.file.en.name == temp_file.name assert instance.file.en.name == temp_file.name
field_dump = pickle.dumps(instance.file) field_dump = pickle.dumps(instance.file)
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
instance.file = pickle.loads(field_dump) instance.file = pickle.loads(field_dump)
assert instance.file.en.field == instance._meta.get_field("file") assert instance.file.en.field == instance._meta.get_field('file')
assert instance.file.en.instance == instance assert instance.file.en.instance == instance
assert isinstance(instance.file.en, LocalizedFieldFile) assert isinstance(instance.file.en, LocalizedFieldFile)
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
instance.file = {"en": ContentFile("test", "testfilename")} instance.file = {'en': ContentFile("test", "testfilename")}
assert isinstance(instance.file.en, LocalizedFieldFile) assert isinstance(instance.file.en, LocalizedFieldFile)
assert instance.file.en.name == "testfilename" assert instance.file.en.name == "testfilename"
another_instance = cls.FileFieldModel() another_instance = cls.FileFieldModel()
another_instance.file = {"ro": instance.file.en} another_instance.file = {'ro': instance.file.en}
assert another_instance == another_instance.file.ro.instance assert another_instance == another_instance.file.ro.instance
assert another_instance.file.ro.lang == "ro" assert another_instance.file.ro.lang == 'ro'
@classmethod @classmethod
def test_save_form_data(cls): def test_save_form_data(cls):
"""Tests whether the :see:save_form_data function correctly set a valid """Tests whether the :see:save_form_data function correctly set
value.""" a valid value."""
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
data = LocalizedFileValue({"en": False}) data = LocalizedFileValue({'en': False})
instance._meta.get_field("file").save_form_data(instance, data) instance._meta.get_field('file').save_form_data(instance, data)
assert instance.file.en == "" assert instance.file.en == ''
@classmethod @classmethod
def test_pre_save(cls): def test_pre_save(cls):
"""Tests whether the :see:pre_save function works properly.""" """Tests whether the :see:pre_save function works properly."""
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
instance.file = {"en": ContentFile("test", "testfilename")} instance.file = {'en': ContentFile("test", "testfilename")}
instance._meta.get_field("file").pre_save(instance, False) instance._meta.get_field('file').pre_save(instance, False)
assert instance.file.en._committed is True assert instance.file.en._committed is True
@classmethod @classmethod
@ -96,8 +99,8 @@ class LocalizedFileFieldTestCase(TestCase):
# Calling delete on an unset FileField should not call the file deletion # Calling delete on an unset FileField should not call the file deletion
# process, but fail silently # process, but fail silently
instance.file.en.delete() instance.file.en.delete()
instance.file.en.save("testfilename", temp_file) instance.file.en.save('testfilename', temp_file)
assert instance.file.en.name == "testfilename" assert instance.file.en.name == 'testfilename'
instance.file.en.delete() instance.file.en.delete()
assert instance.file.en.name is None assert instance.file.en.name is None
@ -107,29 +110,27 @@ class LocalizedFileFieldTestCase(TestCase):
works correctly.""" works correctly."""
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
field = instance._meta.get_field("file") field = instance._meta.get_field('file')
field.upload_to = "{lang}/" field.upload_to = '{lang}/'
filename = field.generate_filename(instance, "test", "en") filename = field.generate_filename(instance, 'test', 'en')
assert filename == "en/test" assert filename == 'en/test'
field.upload_to = lambda instance, filename, lang: "%s_%s" % ( field.upload_to = lambda instance, filename, lang: \
lang, '%s_%s' % (lang, filename)
filename, filename = field.generate_filename(instance, 'test', 'en')
) assert filename == 'en_test'
filename = field.generate_filename(instance, "test", "en")
assert filename == "en_test"
@classmethod @classmethod
@override_settings(LANGUAGES=(("en", "English"),)) @override_settings(LANGUAGES=(('en', 'English'),))
def test_value_to_string(cls): def test_value_to_string(cls):
"""Tests whether the :see:LocalizedFileField class's """Tests whether the :see:LocalizedFileField
:see:value_to_string function works properly.""" class's :see:value_to_string function works properly."""
temp_file = File(tempfile.NamedTemporaryFile()) temp_file = File(tempfile.NamedTemporaryFile())
instance = cls.FileFieldModel() instance = cls.FileFieldModel()
field = cls.FileFieldModel._meta.get_field("file") field = cls.FileFieldModel._meta.get_field('file')
field.upload_to = "" field.upload_to = ''
instance.file.en.save("testfilename", temp_file) instance.file.en.save('testfilename', temp_file)
expected_value_to_string = json.dumps({"en": "testfilename"}) expected_value_to_string = json.dumps({'en': 'testfilename'})
assert field.value_to_string(instance) == expected_value_to_string assert field.value_to_string(instance) == expected_value_to_string
@staticmethod @staticmethod
@ -137,15 +138,15 @@ class LocalizedFileFieldTestCase(TestCase):
"""Tests whether the :see:get_prep_value function returns correctly """Tests whether the :see:get_prep_value function returns correctly
value.""" value."""
value = LocalizedValue({"en": None}) value = LocalizedValue({'en': None})
assert LocalizedFileField().get_prep_value(None) is None assert LocalizedFileField().get_prep_value(None) is None
assert isinstance(LocalizedFileField().get_prep_value(value), dict) assert isinstance(LocalizedFileField().get_prep_value(value), dict)
assert LocalizedFileField().get_prep_value(value)["en"] == "" assert LocalizedFileField().get_prep_value(value)['en'] == ''
@staticmethod @staticmethod
def test_formfield(): def test_formfield():
"""Tests whether the :see:formfield function correctly returns a valid """Tests whether the :see:formfield function correctly returns
form.""" a valid form."""
form_field = LocalizedFileField().formfield() form_field = LocalizedFileField().formfield()
assert isinstance(form_field, LocalizedFileFieldForm) assert isinstance(form_field, LocalizedFileFieldForm)
@ -154,13 +155,11 @@ class LocalizedFileFieldTestCase(TestCase):
@staticmethod @staticmethod
def test_deconstruct(): def test_deconstruct():
"""Tests whether the :see:LocalizedFileField class's :see:deconstruct """Tests whether the :see:LocalizedFileField
function works properly.""" class's :see:deconstruct function works properly."""
name, path, args, kwargs = LocalizedFileField().deconstruct() name, path, args, kwargs = LocalizedFileField().deconstruct()
assert "upload_to" in kwargs assert 'upload_to' in kwargs
assert "storage" not in kwargs assert 'storage' not in kwargs
name, path, args, kwargs = LocalizedFileField( name, path, args, kwargs = LocalizedFileField(storage='test').deconstruct()
storage="test" assert 'storage' in kwargs
).deconstruct()
assert "storage" in kwargs

View File

@ -16,7 +16,7 @@ class LocalizedFileFieldFormTestCase(TestCase):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
formfield.clean([]) formfield.clean([])
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
formfield.clean([], {"en": None}) formfield.clean([], {'en': None})
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
formfield.clean("badvalue") formfield.clean("badvalue")
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -24,17 +24,17 @@ class LocalizedFileFieldFormTestCase(TestCase):
formfield.clean(value) formfield.clean(value)
formfield = LocalizedFileFieldForm(required=False) formfield = LocalizedFileFieldForm(required=False)
formfield.clean([""] * len(settings.LANGUAGES)) formfield.clean([''] * len(settings.LANGUAGES))
formfield.clean(["", ""], ["", ""]) formfield.clean(['', ''], ['', ''])
def test_bound_data(self): def test_bound_data(self):
"""Tests whether the :see:bound_data function is returns correctly """Tests whether the :see:bound_data function is returns correctly
value.""" value"""
formfield = LocalizedFileFieldForm() formfield = LocalizedFileFieldForm()
assert formfield.bound_data([""], None) == [""] assert formfield.bound_data([''], None) == ['']
initial = dict([(lang, "") for lang, _ in settings.LANGUAGES]) initial = dict([(lang, '') for lang, _ in settings.LANGUAGES])
value = [None] * len(settings.LANGUAGES) value = [None] * len(settings.LANGUAGES)
expected_value = [""] * len(settings.LANGUAGES) expected_value = [''] * len(settings.LANGUAGES)
assert formfield.bound_data(value, initial) == expected_value assert formfield.bound_data(value, initial) == expected_value

View File

@ -9,18 +9,16 @@ class LocalizedFileWidgetTestCase(TestCase):
@staticmethod @staticmethod
def test_get_context(): def test_get_context():
"""Tests whether the :see:get_context correctly handles 'required' """Tests whether the :see:get_context correctly
attribute, separately for each subwidget.""" handles 'required' attribute, separately for each subwidget."""
widget = LocalizedFileWidget() widget = LocalizedFileWidget()
widget.widgets[0].is_required = True widget.widgets[0].is_required = True
widget.widgets[1].is_required = True widget.widgets[1].is_required = True
widget.widgets[2].is_required = False widget.widgets[2].is_required = False
context = widget.get_context( context = widget.get_context(name='test',
name="test", value=LocalizedFileValue(dict(en='test')),
value=LocalizedFileValue(dict(en="test")), attrs=dict(required=True))
attrs=dict(required=True), assert 'required' not in context['widget']['subwidgets'][0]['attrs']
) assert context['widget']['subwidgets'][1]['attrs']['required']
assert "required" not in context["widget"]["subwidgets"][0]["attrs"] assert 'required' not in context['widget']['subwidgets'][2]['attrs']
assert context["widget"]["subwidgets"][1]["attrs"]["required"]
assert "required" not in context["widget"]["subwidgets"][2]["attrs"]

View File

@ -1,7 +1,7 @@
from django.test import TestCase
from django.db.utils import IntegrityError
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import translation from django.utils import translation
from localized_fields.fields import LocalizedFloatField from localized_fields.fields import LocalizedFloatField
@ -10,8 +10,8 @@ from .fake_model import get_fake_model
class LocalizedFloatFieldTestCase(TestCase): class LocalizedFloatFieldTestCase(TestCase):
"""Tests whether the :see:LocalizedFloatField and :see:LocalizedFloatValue """Tests whether the :see:LocalizedFloatField
works properly.""" and :see:LocalizedFloatValue works properly."""
TestModel = None TestModel = None
@ -19,7 +19,9 @@ class LocalizedFloatFieldTestCase(TestCase):
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.TestModel = get_fake_model({"score": LocalizedFloatField()}) cls.TestModel = get_fake_model({
'score': LocalizedFloatField()
})
def test_basic(self): def test_basic(self):
"""Tests the basics of storing float values.""" """Tests the basics of storing float values."""
@ -34,8 +36,8 @@ class LocalizedFloatFieldTestCase(TestCase):
assert obj.score.get(lang_code) == index + 1.0 assert obj.score.get(lang_code) == index + 1.0
def test_primary_language_required(self): def test_primary_language_required(self):
"""Tests whether the primary language is required by default and all """Tests whether the primary language is required by
other languages are optiona.""" default and all other languages are optiona."""
# not filling in anything should raise IntegrityError, # not filling in anything should raise IntegrityError,
# the primary language is required # the primary language is required
@ -54,8 +56,8 @@ class LocalizedFloatFieldTestCase(TestCase):
obj.save() obj.save()
def test_default_value_none(self): def test_default_value_none(self):
"""Tests whether the default value for optional languages is """Tests whether the default value for optional languages
NoneType.""" is NoneType."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 1234.0) obj.score.set(settings.LANGUAGE_CODE, 1234.0)
@ -68,8 +70,9 @@ class LocalizedFloatFieldTestCase(TestCase):
assert obj.score.get(lang_code) is None assert obj.score.get(lang_code) is None
def test_translate(self): def test_translate(self):
"""Tests whether casting the value to an float results in the value """Tests whether casting the value to an float
being returned in the currently active language as an float.""" results in the value being returned in the currently
active language as an float."""
obj = self.TestModel() obj = self.TestModel()
for index, (lang_code, _) in enumerate(settings.LANGUAGES): for index, (lang_code, _) in enumerate(settings.LANGUAGES):
@ -83,9 +86,10 @@ class LocalizedFloatFieldTestCase(TestCase):
assert obj.score.translate() == index + 1.0 assert obj.score.translate() == index + 1.0
def test_translate_primary_fallback(self): def test_translate_primary_fallback(self):
"""Tests whether casting the value to an float results in the value """Tests whether casting the value to an float
begin returned in the active language and falls back to the primary results in the value begin returned in the active
language if there is no value in that language.""" language and falls back to the primary language
if there is no value in that language."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25.0) obj.score.set(settings.LANGUAGE_CODE, 25.0)
@ -98,8 +102,9 @@ class LocalizedFloatFieldTestCase(TestCase):
assert float(obj.score) == 25.0 assert float(obj.score) == 25.0
def test_get_default_value(self): def test_get_default_value(self):
"""Tests whether getting the value in a specific language properly """Tests whether getting the value in a specific
returns the specified default in case it is not available.""" language properly returns the specified default
in case it is not available."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25.0) obj.score.set(settings.LANGUAGE_CODE, 25.0)
@ -109,11 +114,12 @@ class LocalizedFloatFieldTestCase(TestCase):
assert obj.score.get(secondary_language, 1337.0) == 1337.0 assert obj.score.get(secondary_language, 1337.0) == 1337.0
def test_completely_optional(self): def test_completely_optional(self):
"""Tests whether having all languages optional works properly.""" """Tests whether having all languages optional
works properly."""
model = get_fake_model( model = get_fake_model({
{"score": LocalizedFloatField(null=True, required=[], blank=True)} 'score': LocalizedFloatField(null=True, required=[], blank=True)
) })
obj = model() obj = model()
obj.save() obj.save()
@ -122,18 +128,19 @@ class LocalizedFloatFieldTestCase(TestCase):
assert getattr(obj.score, lang_code) is None assert getattr(obj.score, lang_code) is None
def test_store_string(self): def test_store_string(self):
"""Tests whether the field properly raises an error when trying to """Tests whether the field properly raises
store a non-float.""" an error when trying to store a non-float."""
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
obj = self.TestModel() obj = self.TestModel()
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
obj.score.set(lang_code, "haha") obj.score.set(lang_code, 'haha')
obj.save() obj.save()
def test_none_if_illegal_value_stored(self): def test_none_if_illegal_value_stored(self):
"""Tests whether None is returned for a language if the value stored in """Tests whether None is returned for a language
the database is not an float.""" if the value stored in the database is not an
float."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25.0) obj.score.set(settings.LANGUAGE_CODE, 25.0)
@ -147,15 +154,12 @@ class LocalizedFloatFieldTestCase(TestCase):
assert obj.score.get(settings.LANGUAGE_CODE) is None assert obj.score.get(settings.LANGUAGE_CODE) is None
def test_default_value(self): def test_default_value(self):
"""Tests whether a default is properly set when specified.""" """Tests whether a default is properly set
when specified."""
model = get_fake_model( model = get_fake_model({
{ 'score': LocalizedFloatField(default={settings.LANGUAGE_CODE: 75.0})
"score": LocalizedFloatField( })
default={settings.LANGUAGE_CODE: 75.0}
)
}
)
obj = model.objects.create() obj = model.objects.create()
assert obj.score.get(settings.LANGUAGE_CODE) == 75.0 assert obj.score.get(settings.LANGUAGE_CODE) == 75.0

View File

@ -9,8 +9,8 @@ class LocalizedFieldFormTestCase(TestCase):
@staticmethod @staticmethod
def test_init(): def test_init():
"""Tests whether the constructor correctly creates a field for every """Tests whether the constructor correctly
language.""" creates a field for every language."""
# case required for specific language # case required for specific language
form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE]) form = LocalizedFieldForm(required=[settings.LANGUAGE_CODE])
@ -42,7 +42,8 @@ class LocalizedFieldFormTestCase(TestCase):
@staticmethod @staticmethod
def test_compress(): def test_compress():
"""Tests whether the :see:compress function is working properly.""" """Tests whether the :see:compress function
is working properly."""
input_value = [lang_name for _, lang_name in settings.LANGUAGES] input_value = [lang_name for _, lang_name in settings.LANGUAGES]
output_value = LocalizedFieldForm().compress(input_value) output_value = LocalizedFieldForm().compress(input_value)

View File

@ -1,20 +1,17 @@
import django from django.test import TestCase
from django.db.utils import IntegrityError
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.utils import IntegrityError
from django.test import TestCase
from django.utils import translation from django.utils import translation
from localized_fields.fields import LocalizedIntegerField from localized_fields.fields import LocalizedIntegerField
from localized_fields.value import LocalizedIntegerValue
from .fake_model import get_fake_model from .fake_model import get_fake_model
class LocalizedIntegerFieldTestCase(TestCase): class LocalizedIntegerFieldTestCase(TestCase):
"""Tests whether the :see:LocalizedIntegerField and """Tests whether the :see:LocalizedIntegerField
:see:LocalizedIntegerValue works properly.""" and :see:LocalizedIntegerValue works properly."""
TestModel = None TestModel = None
@ -22,7 +19,9 @@ class LocalizedIntegerFieldTestCase(TestCase):
def setUpClass(cls): def setUpClass(cls):
super().setUpClass() super().setUpClass()
cls.TestModel = get_fake_model({"score": LocalizedIntegerField()}) cls.TestModel = get_fake_model({
'score': LocalizedIntegerField()
})
def test_basic(self): def test_basic(self):
"""Tests the basics of storing integer values.""" """Tests the basics of storing integer values."""
@ -37,8 +36,8 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.get(lang_code) == index + 1 assert obj.score.get(lang_code) == index + 1
def test_primary_language_required(self): def test_primary_language_required(self):
"""Tests whether the primary language is required by default and all """Tests whether the primary language is required by
other languages are optiona.""" default and all other languages are optiona."""
# not filling in anything should raise IntegrityError, # not filling in anything should raise IntegrityError,
# the primary language is required # the primary language is required
@ -57,8 +56,8 @@ class LocalizedIntegerFieldTestCase(TestCase):
obj.save() obj.save()
def test_default_value_none(self): def test_default_value_none(self):
"""Tests whether the default value for optional languages is """Tests whether the default value for optional languages
NoneType.""" is NoneType."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 1234) obj.score.set(settings.LANGUAGE_CODE, 1234)
@ -71,8 +70,9 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.get(lang_code) is None assert obj.score.get(lang_code) is None
def test_translate(self): def test_translate(self):
"""Tests whether casting the value to an integer results in the value """Tests whether casting the value to an integer
being returned in the currently active language as an integer.""" results in the value being returned in the currently
active language as an integer."""
obj = self.TestModel() obj = self.TestModel()
for index, (lang_code, _) in enumerate(settings.LANGUAGES): for index, (lang_code, _) in enumerate(settings.LANGUAGES):
@ -86,9 +86,10 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.translate() == index + 1 assert obj.score.translate() == index + 1
def test_translate_primary_fallback(self): def test_translate_primary_fallback(self):
"""Tests whether casting the value to an integer results in the value """Tests whether casting the value to an integer
begin returned in the active language and falls back to the primary results in the value begin returned in the active
language if there is no value in that language.""" language and falls back to the primary language
if there is no value in that language."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25) obj.score.set(settings.LANGUAGE_CODE, 25)
@ -101,8 +102,9 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert int(obj.score) == 25 assert int(obj.score) == 25
def test_get_default_value(self): def test_get_default_value(self):
"""Tests whether getting the value in a specific language properly """Tests whether getting the value in a specific
returns the specified default in case it is not available.""" language properly returns the specified default
in case it is not available."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25) obj.score.set(settings.LANGUAGE_CODE, 25)
@ -112,11 +114,12 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.get(secondary_language, 1337) == 1337 assert obj.score.get(secondary_language, 1337) == 1337
def test_completely_optional(self): def test_completely_optional(self):
"""Tests whether having all languages optional works properly.""" """Tests whether having all languages optional
works properly."""
model = get_fake_model( model = get_fake_model({
{"score": LocalizedIntegerField(null=True, required=[], blank=True)} 'score': LocalizedIntegerField(null=True, required=[], blank=True)
) })
obj = model() obj = model()
obj.save() obj.save()
@ -125,18 +128,19 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert getattr(obj.score, lang_code) is None assert getattr(obj.score, lang_code) is None
def test_store_string(self): def test_store_string(self):
"""Tests whether the field properly raises an error when trying to """Tests whether the field properly raises
store a non-integer.""" an error when trying to store a non-integer."""
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
obj = self.TestModel() obj = self.TestModel()
with self.assertRaises(IntegrityError): with self.assertRaises(IntegrityError):
obj.score.set(lang_code, "haha") obj.score.set(lang_code, 'haha')
obj.save() obj.save()
def test_none_if_illegal_value_stored(self): def test_none_if_illegal_value_stored(self):
"""Tests whether None is returned for a language if the value stored in """Tests whether None is returned for a language
the database is not an integer.""" if the value stored in the database is not an
integer."""
obj = self.TestModel() obj = self.TestModel()
obj.score.set(settings.LANGUAGE_CODE, 25) obj.score.set(settings.LANGUAGE_CODE, 25)
@ -150,15 +154,12 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.get(settings.LANGUAGE_CODE) is None assert obj.score.get(settings.LANGUAGE_CODE) is None
def test_default_value(self): def test_default_value(self):
"""Tests whether a default is properly set when specified.""" """Tests whether a default is properly set
when specified."""
model = get_fake_model( model = get_fake_model({
{ 'score': LocalizedIntegerField(default={settings.LANGUAGE_CODE: 75})
"score": LocalizedIntegerField( })
default={settings.LANGUAGE_CODE: 75}
)
}
)
obj = model.objects.create() obj = model.objects.create()
assert obj.score.get(settings.LANGUAGE_CODE) == 75 assert obj.score.get(settings.LANGUAGE_CODE) == 75
@ -173,67 +174,3 @@ class LocalizedIntegerFieldTestCase(TestCase):
assert obj.score.get(lang_code) == 75 assert obj.score.get(lang_code) == 75
else: else:
assert obj.score.get(lang_code) is None assert obj.score.get(lang_code) is None
def test_default_value_update(self):
"""Tests whether a default is properly set when specified during
updates."""
model = get_fake_model(
{
"score": LocalizedIntegerField(
default={settings.LANGUAGE_CODE: 75}, null=True
)
}
)
obj = model.objects.create(
score=LocalizedIntegerValue({settings.LANGUAGE_CODE: 35})
)
assert obj.score.get(settings.LANGUAGE_CODE) == 35
model.objects.update(
score=LocalizedIntegerValue({settings.LANGUAGE_CODE: None})
)
obj.refresh_from_db()
assert obj.score.get(settings.LANGUAGE_CODE) == 75
def test_callable_default_value(self):
output = {"en": 5}
def func():
return output
model = get_fake_model({"test": LocalizedIntegerField(default=func)})
obj = model.objects.create()
assert obj.test["en"] == output["en"]
def test_order_by(self):
"""Tests whether ordering by a :see:LocalizedIntegerField key works
expected."""
# using key transforms (score__en) in order_by(..) is only
# supported since Django 2.1
# https://github.com/django/django/commit/2162f0983de0dfe2178531638ce7ea56f54dd4e7#diff-0edd853580d56db07e4020728d59e193
if django.VERSION < (2, 1):
return
model = get_fake_model(
{
"score": LocalizedIntegerField(
default={settings.LANGUAGE_CODE: 1337}, null=True
)
}
)
model.objects.create(score=dict(en=982))
model.objects.create(score=dict(en=382))
model.objects.create(score=dict(en=1331))
res = list(
model.objects.values_list("score__en", flat=True).order_by(
"-score__en"
)
)
assert res == [1331, 982, 382]

View File

@ -1,51 +0,0 @@
import django
import pytest
from django.test import TestCase
from localized_fields.fields import LocalizedField
from localized_fields.value import LocalizedValue
from .fake_model import get_fake_model
class LocalizedIsNullLookupsTestCase(TestCase):
"""Tests whether ref lookups properly work with."""
TestModel1 = None
@classmethod
def setUpClass(cls):
"""Creates the test model in the database."""
super(LocalizedIsNullLookupsTestCase, cls).setUpClass()
cls.TestModel = get_fake_model(
{"text": LocalizedField(null=True, required=[])}
)
cls.TestModel.objects.create(
text=LocalizedValue(dict(en="text_en", ro="text_ro", nl="text_nl"))
)
cls.TestModel.objects.create(
text=None,
)
def test_isnull_lookup_valid_values(self):
"""Test whether isnull properly works with valid values."""
assert self.TestModel.objects.filter(text__isnull=True).exists()
assert self.TestModel.objects.filter(text__isnull=False).exists()
def test_isnull_lookup_null(self):
"""Test whether isnull crashes with None as value."""
with pytest.raises(ValueError):
assert self.TestModel.objects.filter(text__isnull=None).exists()
def test_isnull_lookup_string(self):
"""Test whether isnull properly works with string values on the
corresponding Django version."""
if django.VERSION < (4, 0):
assert self.TestModel.objects.filter(text__isnull="True").exists()
else:
with pytest.raises(ValueError):
assert self.TestModel.objects.filter(
text__isnull="True"
).exists()

View File

@ -3,7 +3,6 @@ from django.conf import settings
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from django.utils import translation from django.utils import translation
from localized_fields.expressions import LocalizedRef
from localized_fields.fields import LocalizedField from localized_fields.fields import LocalizedField
from localized_fields.value import LocalizedValue from localized_fields.value import LocalizedValue
@ -13,7 +12,6 @@ from .fake_model import get_fake_model
@override_settings(LOCALIZED_FIELDS_EXPERIMENTAL=True) @override_settings(LOCALIZED_FIELDS_EXPERIMENTAL=True)
class LocalizedLookupsTestCase(TestCase): class LocalizedLookupsTestCase(TestCase):
"""Tests whether localized lookups properly work with.""" """Tests whether localized lookups properly work with."""
TestModel1 = None TestModel1 = None
@classmethod @classmethod
@ -23,109 +21,31 @@ class LocalizedLookupsTestCase(TestCase):
super(LocalizedLookupsTestCase, cls).setUpClass() super(LocalizedLookupsTestCase, cls).setUpClass()
# reload app as setting has changed # reload app as setting has changed
config = apps.get_app_config("localized_fields") config = apps.get_app_config('localized_fields')
config.ready() config.ready()
cls.TestModel = get_fake_model({"text": LocalizedField()}) cls.TestModel = get_fake_model(
{
'text': LocalizedField(),
}
)
def test_localized_lookup(self): def test_localized_lookup(self):
"""Tests whether localized lookup properly works.""" """Tests whether localized lookup properly works."""
self.TestModel.objects.create( self.TestModel.objects.create(
text=LocalizedValue(dict(en="text_en", ro="text_ro", nl="text_nl")) text=LocalizedValue(dict(en='text_en', ro='text_ro', nl='text_nl')),
) )
# assert that it properly lookups the currently active language # assert that it properly lookups the currently active language
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
translation.activate(lang_code) translation.activate(lang_code)
assert self.TestModel.objects.filter( assert self.TestModel.objects.filter(text='text_' + lang_code).exists()
text="text_" + lang_code
).exists()
# ensure that the default language is used in case no # ensure that the default language is used in case no
# language is active at all # language is active at all
translation.deactivate_all() translation.deactivate_all()
assert self.TestModel.objects.filter(text="text_en").exists() assert self.TestModel.objects.filter(text='text_en').exists()
# ensure that hstore lookups still work # ensure that hstore lookups still work
assert self.TestModel.objects.filter(text__ro="text_ro").exists() assert self.TestModel.objects.filter(text__ro='text_ro').exists()
def test_localized_lookup_specific_isnull(self):
self.TestModel.objects.create(
text=LocalizedValue(dict(en="text_en", ro="text_ro", nl=None))
)
translation.activate("nl")
assert (
self.TestModel.objects.annotate(text_localized=LocalizedRef("text"))
.filter(text_localized__isnull=True)
.exists()
)
class LocalizedRefLookupsTestCase(TestCase):
"""Tests whether ref lookups properly work with."""
TestModel1 = None
@classmethod
def setUpClass(cls):
"""Creates the test model in the database."""
super(LocalizedRefLookupsTestCase, cls).setUpClass()
cls.TestModel = get_fake_model({"text": LocalizedField()})
cls.TestModel.objects.create(
text=LocalizedValue(dict(en="text_en", ro="text_ro", nl="text_nl"))
)
def test_active_ref_lookup(self):
"""Tests whether active_ref lookup properly works."""
# assert that it properly lookups the currently active language
for lang_code, _ in settings.LANGUAGES:
translation.activate(lang_code)
assert self.TestModel.objects.filter(
text__active_ref=f"text_{lang_code}"
).exists()
# ensure that the default language is used in case no
# language is active at all
translation.deactivate_all()
assert self.TestModel.objects.filter(
text__active_ref="text_en"
).exists()
def test_translated_ref_lookup(self):
"""Tests whether translated_ref lookup properly works."""
# assert that it properly lookups the currently active language
for lang_code, _ in settings.LANGUAGES:
translation.activate(lang_code)
assert self.TestModel.objects.filter(
text__translated_ref=f"text_{lang_code}"
).exists()
# ensure that the default language is used in case no
# language is active at all
translation.deactivate_all()
assert self.TestModel.objects.filter(
text__translated_ref="text_en"
).exists()
fallbacks = {"cs": ["ru", "ro"], "pl": ["nl", "ro"]}
with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks):
with translation.override("cs"):
assert self.TestModel.objects.filter(
text__translated_ref="text_ro"
).exists()
with translation.override("pl"):
assert self.TestModel.objects.filter(
text__translated_ref="text_nl"
).exists()
# ensure that the default language is used in case no fallback is set
with translation.override("ru"):
assert self.TestModel.objects.filter(
text__translated_ref="text_en"
).exists()

View File

@ -17,12 +17,17 @@ class LocalizedModelTestCase(TestCase):
super(LocalizedModelTestCase, cls).setUpClass() super(LocalizedModelTestCase, cls).setUpClass()
cls.TestModel = get_fake_model({"title": LocalizedField()}) cls.TestModel = get_fake_model(
{
'title': LocalizedField()
}
)
@classmethod @classmethod
def test_defaults(cls): def test_defaults(cls):
"""Tests whether all :see:LocalizedField fields are assigned an empty """Tests whether all :see:LocalizedField
:see:LocalizedValue instance when the model is instanitiated.""" fields are assigned an empty :see:LocalizedValue
instance when the model is instanitiated."""
obj = cls.TestModel() obj = cls.TestModel()
@ -30,18 +35,19 @@ class LocalizedModelTestCase(TestCase):
@classmethod @classmethod
def test_model_init_kwargs(cls): def test_model_init_kwargs(cls):
"""Tests whether all :see:LocalizedField fields are assigned an empty """Tests whether all :see:LocalizedField
:see:LocalizedValue instance when the model is instanitiated.""" fields are assigned an empty :see:LocalizedValue
instance when the model is instanitiated."""
data = { data = {
"title": { 'title': {
"en": "english_title", 'en': 'english_title',
"ro": "romanian_title", 'ro': 'romanian_title',
"nl": "dutch_title", 'nl': 'dutch_title'
} }
} }
obj = cls.TestModel(**data) obj = cls.TestModel(**data)
assert isinstance(obj.title, LocalizedValue) assert isinstance(obj.title, LocalizedValue)
assert obj.title.en == "english_title" assert obj.title.en == 'english_title'
assert obj.title.ro == "romanian_title" assert obj.title.ro == 'romanian_title'
assert obj.title.nl == "dutch_title" assert obj.title.nl == 'dutch_title'

View File

@ -16,24 +16,30 @@ class LocalizedQuerySetTestCase(TestCase):
super(LocalizedQuerySetTestCase, cls).setUpClass() super(LocalizedQuerySetTestCase, cls).setUpClass()
cls.Model = get_fake_model({"title": LocalizedField()}) cls.Model = get_fake_model(
{
'title': LocalizedField(),
}
)
@classmethod @classmethod
def test_assign_raw_dict(cls): def test_assign_raw_dict(cls):
inst = cls.Model() inst = cls.Model()
inst.title = dict(en="Bread", ro="Paine") inst.title = dict(en='Bread', ro='Paine')
inst.save() inst.save()
inst = cls.Model.objects.get(pk=inst.pk) inst = cls.Model.objects.get(pk=inst.pk)
assert inst.title.en == "Bread" assert inst.title.en == 'Bread'
assert inst.title.ro == "Paine" assert inst.title.ro == 'Paine'
@classmethod @classmethod
def test_assign_raw_dict_update(cls): def test_assign_raw_dict_update(cls):
inst = cls.Model.objects.create(title=dict(en="Bread", ro="Paine")) inst = cls.Model.objects.create(
title=dict(en='Bread', ro='Paine'))
cls.Model.objects.update(title=dict(en="Beer", ro="Bere")) cls.Model.objects.update(
title=dict(en='Beer', ro='Bere'))
inst = cls.Model.objects.get(pk=inst.pk) inst = cls.Model.objects.get(pk=inst.pk)
assert inst.title.en == "Beer" assert inst.title.en == 'Beer'
assert inst.title.ro == "Bere" assert inst.title.ro == 'Bere'

View File

@ -1,15 +1,16 @@
import copy import copy
import pytest
from django import forms from django import forms
from django.conf import settings
from django.db import models from django.db import models
from django.db.utils import IntegrityError from django.conf import settings
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError
from django.utils.text import slugify from django.utils.text import slugify
from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField from localized_fields.fields import (
LocalizedField,
LocalizedUniqueSlugField
)
from .fake_model import get_fake_model from .fake_model import get_fake_model
@ -28,25 +29,23 @@ class LocalizedSlugFieldTestCase(TestCase):
cls.Model = get_fake_model( cls.Model = get_fake_model(
{ {
"title": LocalizedField(), 'title': LocalizedField(),
"name": models.CharField(max_length=255), 'name': models.CharField(max_length=255),
"slug": LocalizedUniqueSlugField(populate_from="title"), 'slug': LocalizedUniqueSlugField(populate_from='title')
} }
) )
@staticmethod @staticmethod
def test_unique_slug_with_time(): def test_unique_slug_with_time():
"""Tests whether the primary key is included in the slug when the """Tests whether the primary key is included in
'use_pk' option is enabled.""" the slug when the 'use_pk' option is enabled."""
title = "myuniquetitle" title = 'myuniquetitle'
PkModel = get_fake_model( PkModel = get_fake_model(
{ {
"title": LocalizedField(), 'title': LocalizedField(),
"slug": LocalizedUniqueSlugField( 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True)
populate_from="title", include_time=True
),
} }
) )
@ -54,7 +53,7 @@ class LocalizedSlugFieldTestCase(TestCase):
obj.title.en = title obj.title.en = title
obj.save() obj.save()
assert obj.slug.en.startswith("%s-" % title) assert obj.slug.en.startswith('%s-' % title)
@classmethod @classmethod
def test_uniue_slug_no_change(cls): def test_uniue_slug_no_change(cls):
@ -62,14 +61,12 @@ class LocalizedSlugFieldTestCase(TestCase):
NoChangeSlugModel = get_fake_model( NoChangeSlugModel = get_fake_model(
{ {
"title": LocalizedField(), 'title': LocalizedField(),
"slug": LocalizedUniqueSlugField( 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True)
populate_from="title", include_time=True
),
} }
) )
title = "myuniquetitle" title = 'myuniquetitle'
obj = NoChangeSlugModel() obj = NoChangeSlugModel()
obj.title.en = title obj.title.en = title
@ -78,7 +75,7 @@ class LocalizedSlugFieldTestCase(TestCase):
old_slug_en = copy.deepcopy(obj.slug.en) old_slug_en = copy.deepcopy(obj.slug.en)
old_slug_nl = copy.deepcopy(obj.slug.nl) old_slug_nl = copy.deepcopy(obj.slug.nl)
obj.title.nl += "beer" obj.title.nl += 'beer'
obj.save() obj.save()
assert old_slug_en == obj.slug.en assert old_slug_en == obj.slug.en
@ -86,20 +83,18 @@ class LocalizedSlugFieldTestCase(TestCase):
@classmethod @classmethod
def test_unique_slug_update(cls): def test_unique_slug_update(cls):
obj = cls.Model.objects.create( obj = cls.Model.objects.create(title={settings.LANGUAGE_CODE: 'mytitle'})
title={settings.LANGUAGE_CODE: "mytitle"} assert obj.slug.get() == 'mytitle'
) obj.title.set(settings.LANGUAGE_CODE, 'othertitle')
assert obj.slug.get() == "mytitle"
obj.title.set(settings.LANGUAGE_CODE, "othertitle")
obj.save() obj.save()
assert obj.slug.get() == "othertitle" assert obj.slug.get() == 'othertitle'
@classmethod @classmethod
def test_unique_slug_unique_max_retries(cls): def test_unique_slug_unique_max_retries(cls):
"""Tests whether the unique slug implementation doesn't try to find a """Tests whether the unique slug implementation doesn't
slug forever and gives up after a while.""" try to find a slug forever and gives up after a while."""
title = "myuniquetitle" title = 'myuniquetitle'
obj = cls.Model() obj = cls.Model()
obj.title.en = title obj.title.en = title
@ -116,159 +111,106 @@ class LocalizedSlugFieldTestCase(TestCase):
"""Tests whether the populating feature works correctly.""" """Tests whether the populating feature works correctly."""
obj = cls.Model() obj = cls.Model()
obj.title.en = "this is my title" obj.title.en = 'this is my title'
obj.save() obj.save()
assert obj.slug.get("en") == slugify(obj.title) assert obj.slug.get('en') == slugify(obj.title)
@classmethod @classmethod
def test_populate_callable(cls): def test_populate_callable(cls):
"""Tests whether the populating feature works correctly when you """Tests whether the populating feature works correctly
specify a callable.""" when you specify a callable."""
def generate_slug(instance): def generate_slug(instance):
return instance.title return instance.title
get_fake_model( get_fake_model({
{ 'title': LocalizedField(),
"title": LocalizedField(), 'slug': LocalizedUniqueSlugField(populate_from=generate_slug)
"slug": LocalizedUniqueSlugField(populate_from=generate_slug), })
}
)
obj = cls.Model() obj = cls.Model()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
obj.title.set(lang_code, "title %s" % lang_name) obj.title.set(lang_code, 'title %s' % lang_name)
obj.save() obj.save()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == "title-%s" % lang_name.lower() assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower()
@staticmethod @staticmethod
def test_populate_multiple_from_fields(): def test_populate_multiple_from_fields():
"""Tests whether populating the slug from multiple fields works """Tests whether populating the slug from multiple
correctly.""" fields works correctly."""
model = get_fake_model( model = get_fake_model(
{ {
"title": LocalizedField(), 'title': LocalizedField(),
"name": models.CharField(max_length=255), 'name': models.CharField(max_length=255),
"slug": LocalizedUniqueSlugField( 'slug': LocalizedUniqueSlugField(populate_from=('title', 'name'))
populate_from=("title", "name")
),
} }
) )
obj = model() obj = model()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
obj.name = "swen" obj.name = 'swen'
obj.title.set(lang_code, "title %s" % lang_name) obj.title.set(lang_code, 'title %s' % lang_name)
obj.save() obj.save()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
assert ( assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower()
obj.slug.get(lang_code) == "title-%s-swen" % lang_name.lower()
)
@staticmethod @staticmethod
def test_populate_multiple_from_fields_fk(): def test_populate_multiple_from_fields_fk():
"""Tests whether populating the slug from multiple fields works """Tests whether populating the slug from multiple
correctly.""" fields works correctly."""
model_fk = get_fake_model({"name": LocalizedField()}) model_fk = get_fake_model(
model = get_fake_model(
{ {
"title": LocalizedField(), 'name': LocalizedField(),
"other": models.ForeignKey(model_fk, on_delete=models.CASCADE),
"slug": LocalizedUniqueSlugField(
populate_from=("title", "other.name")
),
} }
) )
other = model_fk.objects.create(name={settings.LANGUAGE_CODE: "swen"}) model = get_fake_model(
{
'title': LocalizedField(),
'other': models.ForeignKey(model_fk, on_delete=models.CASCADE),
'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name'))
}
)
other = model_fk.objects.create(name={settings.LANGUAGE_CODE: 'swen'})
obj = model() obj = model()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
obj.other_id = other.id obj.other_id = other.id
obj.title.set(lang_code, "title %s" % lang_name) obj.title.set(lang_code, 'title %s' % lang_name)
obj.save() obj.save()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
assert ( assert obj.slug.get(lang_code) == 'title-%s-swen' % lang_name.lower()
obj.slug.get(lang_code) == "title-%s-swen" % lang_name.lower()
)
@classmethod @classmethod
def test_populate_multiple_languages(cls): def test_populate_multiple_languages(cls):
"""Tests whether the populating feature correctly works for all """Tests whether the populating feature correctly
languages.""" works for all languages."""
obj = cls.Model() obj = cls.Model()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
obj.title.set(lang_code, "title %s" % lang_name) obj.title.set(lang_code, 'title %s' % lang_name)
obj.save() obj.save()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == "title-%s" % lang_name.lower() assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower()
@classmethod
def test_disable(cls):
"""Tests whether disabling auto-slugging works."""
Model = get_fake_model(
{
"title": LocalizedField(),
"slug": LocalizedUniqueSlugField(
populate_from="title", enabled=False
),
}
)
obj = Model()
obj.title = "test"
# should raise IntegrityError because auto-slugging
# is disabled and the slug field is NULL
with pytest.raises(IntegrityError):
obj.save()
@classmethod
def test_allows_override_when_immutable(cls):
"""Tests whether setting a value manually works and does not get
overriden."""
Model = get_fake_model(
{
"title": LocalizedField(),
"name": models.CharField(max_length=255),
"slug": LocalizedUniqueSlugField(
populate_from="title", immutable=True
),
}
)
obj = Model()
for lang_code, lang_name in settings.LANGUAGES:
obj.slug.set(lang_code, "my value %s" % lang_code)
obj.title.set(lang_code, "my title %s" % lang_code)
obj.save()
for lang_code, lang_name in settings.LANGUAGES:
assert obj.slug.get(lang_code) == "my value %s" % lang_code
@classmethod @classmethod
def test_unique_slug(cls): def test_unique_slug(cls):
"""Tests whether unique slugs are properly generated.""" """Tests whether unique slugs are properly generated."""
title = "myuniquetitle" title = 'myuniquetitle'
obj = cls.Model() obj = cls.Model()
obj.title.en = title obj.title.en = title
@ -279,39 +221,38 @@ class LocalizedSlugFieldTestCase(TestCase):
another_obj.title.en = title another_obj.title.en = title
another_obj.save() another_obj.save()
assert another_obj.slug.en == "%s-%d" % (title, i) assert another_obj.slug.en == '%s-%d' % (title, i)
@classmethod @classmethod
def test_unique_slug_utf(cls): def test_unique_slug_utf(cls):
"""Tests whether generating a slug works when the value consists """Tests whether generating a slug works
completely out of non-ASCII characters.""" when the value consists completely out
of non-ASCII characters."""
obj = cls.Model() obj = cls.Model()
obj.title.en = "مكاتب للايجار بشارع بورسعيد" obj.title.en = 'مكاتب للايجار بشارع بورسعيد'
obj.save() obj.save()
assert obj.slug.en == "مكاتب-للايجار-بشارع-بورسعيد" assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد'
@staticmethod @staticmethod
def test_deconstruct(): def test_deconstruct():
"""Tests whether the :see:deconstruct function properly retains options """Tests whether the :see:deconstruct
function properly retains options
specified in the constructor.""" specified in the constructor."""
field = LocalizedUniqueSlugField( field = LocalizedUniqueSlugField(populate_from='title')
enabled=False, immutable=True, populate_from="title"
)
_, _, _, kwargs = field.deconstruct() _, _, _, kwargs = field.deconstruct()
assert not kwargs["enabled"] assert 'populate_from' in kwargs
assert kwargs["immutable"] assert kwargs['populate_from'] == field.populate_from
assert kwargs["populate_from"] == field.populate_from
@staticmethod @staticmethod
def test_formfield(): def test_formfield():
"""Tests whether the :see:formfield method returns a valid form field """Tests whether the :see:formfield method
that is hidden.""" returns a valid form field that is hidden."""
form_field = LocalizedUniqueSlugField(populate_from="title").formfield() form_field = LocalizedUniqueSlugField(populate_from='title').formfield()
assert isinstance(form_field, forms.CharField) assert isinstance(form_field, forms.CharField)
assert isinstance(form_field.widget, forms.HiddenInput) assert isinstance(form_field.widget, forms.HiddenInput)

View File

@ -1,7 +1,7 @@
from django.conf import settings
from django.db.models import F from django.db.models import F
from django.test import TestCase, override_settings from django.conf import settings
from django.utils import translation from django.utils import translation
from django.test import TestCase, override_settings
from localized_fields.value import LocalizedValue from localized_fields.value import LocalizedValue
@ -13,14 +13,16 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def tearDown(): def tearDown():
"""Assures that the current language is set back to the default.""" """Assures that the current language
is set back to the default."""
translation.activate(settings.LANGUAGE_CODE) translation.activate(settings.LANGUAGE_CODE)
@staticmethod @staticmethod
def test_init(): def test_init():
"""Tests whether the __init__ function of the :see:LocalizedValue class """Tests whether the __init__ function
works as expected.""" of the :see:LocalizedValue class works
as expected."""
keys = get_init_values() keys = get_init_values()
value = LocalizedValue(keys) value = LocalizedValue(keys)
@ -30,41 +32,32 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_init_default_values(): def test_init_default_values():
"""Tests whether the __init__ function of the :see:LocalizedValue """Tests whether the __init__ function
accepts the default value or an empty dict properly.""" of the :see:LocalizedValue accepts the
default value or an empty dict properly."""
value = LocalizedValue() value = LocalizedValue()
for lang_code, _ in settings.LANGUAGES: for lang_code, _ in settings.LANGUAGES:
assert getattr(value, lang_code) is None assert getattr(value, lang_code) is None
@staticmethod
def test_is_empty():
"""Tests whether a newly constructed :see:LocalizedValue without any
content is considered "empty"."""
value = LocalizedValue()
assert value.is_empty()
value.set(settings.LANGUAGE_CODE, "my value")
assert not value.is_empty()
@staticmethod @staticmethod
def test_init_array(): def test_init_array():
"""Tests whether the __init__ function of :see:LocalizedValue properly """Tests whether the __init__ function
handles an array. of :see:LocalizedValue properly handles an
array.
Arrays can be passed to LocalizedValue as a result of a ArrayAgg Arrays can be passed to LocalizedValue as
operation. a result of a ArrayAgg operation."""
"""
value = LocalizedValue(["my value"]) value = LocalizedValue(['my value'])
assert value.get(settings.LANGUAGE_CODE) == "my value" assert value.get(settings.LANGUAGE_CODE) == 'my value'
@staticmethod @staticmethod
def test_get_explicit(): def test_get_explicit():
"""Tests whether the the :see:LocalizedValue class's :see:get function """Tests whether the the :see:LocalizedValue
works properly when specifying an explicit value.""" class's :see:get function works properly
when specifying an explicit value."""
keys = get_init_values() keys = get_init_values()
localized_value = LocalizedValue(keys) localized_value = LocalizedValue(keys)
@ -74,8 +67,9 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_get_default_language(): def test_get_default_language():
"""Tests whether the :see:LocalizedValue class's see:get function """Tests whether the :see:LocalizedValue
properly gets the value in the default language.""" class's see:get function properly
gets the value in the default language."""
keys = get_init_values() keys = get_init_values()
localized_value = LocalizedValue(keys) localized_value = LocalizedValue(keys)
@ -86,8 +80,8 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_set(): def test_set():
"""Tests whether the :see:LocalizedValue class's see:set function works """Tests whether the :see:LocalizedValue
properly.""" class's see:set function works properly."""
localized_value = LocalizedValue() localized_value = LocalizedValue()
@ -98,21 +92,21 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_eq(): def test_eq():
"""Tests whether the __eq__ operator of :see:LocalizedValue works """Tests whether the __eq__ operator
properly.""" of :see:LocalizedValue works properly."""
a = LocalizedValue({"en": "a", "ar": "b"}) a = LocalizedValue({'en': 'a', 'ar': 'b'})
b = LocalizedValue({"en": "a", "ar": "b"}) b = LocalizedValue({'en': 'a', 'ar': 'b'})
assert a == b assert a == b
b.en = "b" b.en = 'b'
assert a != b assert a != b
@staticmethod @staticmethod
def test_translate(): def test_translate():
"""Tests whether the :see:LocalizedValue class's __str__ works """Tests whether the :see:LocalizedValue
properly.""" class's __str__ works properly."""
keys = get_init_values() keys = get_init_values()
localized_value = LocalizedValue(keys) localized_value = LocalizedValue(keys)
@ -123,12 +117,15 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_translate_fallback(): def test_translate_fallback():
"""Tests whether the :see:LocalizedValue class's translate()'s fallback """Tests whether the :see:LocalizedValue
functionality works properly.""" class's translate()'s fallback functionality
works properly."""
test_value = "myvalue" test_value = 'myvalue'
localized_value = LocalizedValue({settings.LANGUAGE_CODE: test_value}) localized_value = LocalizedValue({
settings.LANGUAGE_CODE: test_value
})
other_language = settings.LANGUAGES[-1][0] other_language = settings.LANGUAGES[-1][0]
@ -149,56 +146,50 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_translate_none(): def test_translate_none():
"""Tests whether the :see:LocalizedValue class's translate() method """Tests whether the :see:LocalizedValue
properly returns None when there is no value.""" class's translate() method properly returns
None when there is no value."""
# with no value, we always expect it to return None # with no value, we always expect it to return None
localized_value = LocalizedValue() localized_value = LocalizedValue()
assert localized_value.translate() is None assert localized_value.translate() is None
assert str(localized_value) == "" assert str(localized_value) == ''
# with no value for the default language, the default # with no value for the default language, the default
# behavior is to return None, unless a custom fallback # behavior is to return None, unless a custom fallback
# chain is configured, which there is not for this test # chain is configured, which there is not for this test
other_language = settings.LANGUAGES[-1][0] other_language = settings.LANGUAGES[-1][0]
localized_value = LocalizedValue({other_language: "hey"}) localized_value = LocalizedValue({
other_language: 'hey'
})
translation.activate(settings.LANGUAGE_CODE) translation.activate(settings.LANGUAGE_CODE)
assert localized_value.translate() is None assert localized_value.translate() is None
assert str(localized_value) == "" assert str(localized_value) == ''
@staticmethod @staticmethod
def test_translate_fallback_custom_fallback(): def test_translate_fallback_custom_fallback():
"""Tests whether the :see:LocalizedValue class's translate()'s fallback """Tests whether the :see:LocalizedValue class's
functionality properly respects the LOCALIZED_FIELDS_FALLBACKS translate()'s fallback functionality properly respects
setting.""" the LOCALIZED_FIELDS_FALLBACKS setting."""
fallbacks = {"nl": ["ro"]} fallbacks = {
'nl': ['ro']
}
localized_value = LocalizedValue( localized_value = LocalizedValue({
{settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, "ro": "ro"} settings.LANGUAGE_CODE: settings.LANGUAGE_CODE,
) 'ro': 'ro'
})
with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks): with override_settings(LOCALIZED_FIELDS_FALLBACKS=fallbacks):
with translation.override("nl"): with translation.override('nl'):
assert localized_value.translate() == "ro" assert localized_value.translate() == 'ro'
@staticmethod
def test_translate_custom_language():
"""Tests whether the :see:LocalizedValue class's translate() ignores
the active language when one is specified explicitely."""
localized_value = LocalizedValue(
{settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, "ro": "ro"}
)
with translation.override("en"):
assert localized_value.translate("ro") == "ro"
@staticmethod @staticmethod
def test_deconstruct(): def test_deconstruct():
"""Tests whether the :see:LocalizedValue class's :see:deconstruct """Tests whether the :see:LocalizedValue
function works properly.""" class's :see:deconstruct function works properly."""
keys = get_init_values() keys = get_init_values()
value = LocalizedValue(keys) value = LocalizedValue(keys)
@ -209,16 +200,16 @@ class LocalizedValueTestCase(TestCase):
@staticmethod @staticmethod
def test_construct_string(): def test_construct_string():
"""Tests whether the :see:LocalizedValue's constructor assumes the """Tests whether the :see:LocalizedValue's constructor
primary language when passing a single string.""" assumes the primary language when passing a single string."""
value = LocalizedValue("beer") value = LocalizedValue('beer')
assert value.get(settings.LANGUAGE_CODE) == "beer" assert value.get(settings.LANGUAGE_CODE) == 'beer'
@staticmethod @staticmethod
def test_construct_expression(): def test_construct_expression():
"""Tests whether passing expressions as values works properly and are """Tests whether passing expressions as values
not converted to string.""" works properly and are not converted to string."""
value = LocalizedValue(dict(en=F("other"))) value = LocalizedValue(dict(en=F('other')))
assert isinstance(value.en, F) assert isinstance(value.en, F)

View File

@ -1,5 +1,4 @@
import re import re
from django.conf import settings from django.conf import settings
from django.test import TestCase from django.test import TestCase
@ -12,7 +11,8 @@ class LocalizedFieldWidgetTestCase(TestCase):
@staticmethod @staticmethod
def test_widget_creation(): def test_widget_creation():
"""Tests whether a widget is created for every language correctly.""" """Tests whether a widget is created for every
language correctly."""
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
assert len(widget.widgets) == len(settings.LANGUAGES) assert len(widget.widgets) == len(settings.LANGUAGES)
@ -20,8 +20,9 @@ class LocalizedFieldWidgetTestCase(TestCase):
@staticmethod @staticmethod
def test_decompress(): def test_decompress():
"""Tests whether a :see:LocalizedValue instance can correctly be """Tests whether a :see:LocalizedValue instance
"decompressed" over the available widgets.""" can correctly be "decompressed" over the available
widgets."""
localized_value = LocalizedValue() localized_value = LocalizedValue()
for lang_code, lang_name in settings.LANGUAGES: for lang_code, lang_name in settings.LANGUAGES:
@ -30,15 +31,13 @@ class LocalizedFieldWidgetTestCase(TestCase):
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
decompressed_values = widget.decompress(localized_value) decompressed_values = widget.decompress(localized_value)
for (lang_code, _), value in zip( for (lang_code, _), value in zip(settings.LANGUAGES, decompressed_values):
settings.LANGUAGES, decompressed_values
):
assert localized_value.get(lang_code) == value assert localized_value.get(lang_code) == value
@staticmethod @staticmethod
def test_decompress_none(): def test_decompress_none():
"""Tests whether the :see:LocalizedFieldWidget correctly handles """Tests whether the :see:LocalizedFieldWidget correctly
:see:None.""" handles :see:None."""
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
decompressed_values = widget.decompress(None) decompressed_values = widget.decompress(None)
@ -48,17 +47,16 @@ class LocalizedFieldWidgetTestCase(TestCase):
@staticmethod @staticmethod
def test_get_context_required(): def test_get_context_required():
"""Tests whether the :see:get_context correctly handles 'required' """Tests whether the :see:get_context correctly
attribute, separately for each subwidget.""" handles 'required' attribute, separately for each subwidget."""
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
widget.widgets[0].is_required = True widget.widgets[0].is_required = True
widget.widgets[1].is_required = False widget.widgets[1].is_required = False
context = widget.get_context( context = widget.get_context(name='test', value=LocalizedValue(),
name="test", value=LocalizedValue(), attrs=dict(required=True) attrs=dict(required=True))
) assert context['widget']['subwidgets'][0]['attrs']['required']
assert context["widget"]["subwidgets"][0]["attrs"]["required"] assert 'required' not in context['widget']['subwidgets'][1]['attrs']
assert "required" not in context["widget"]["subwidgets"][1]["attrs"]
@staticmethod @staticmethod
def test_get_context_langs(): def test_get_context_langs():
@ -66,20 +64,20 @@ class LocalizedFieldWidgetTestCase(TestCase):
'lang_name' attribute for each subwidget.""" 'lang_name' attribute for each subwidget."""
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
context = widget.get_context( context = widget.get_context(name='test', value=LocalizedValue(),
name="test", value=LocalizedValue(), attrs=dict() attrs=dict())
) subwidgets_context = context['widget']['subwidgets']
subwidgets_context = context["widget"]["subwidgets"]
for widget, context in zip(widget.widgets, subwidgets_context): for widget, context in zip(widget.widgets, subwidgets_context):
assert "lang_code" in context assert 'lang_code' in context
assert "lang_name" in context assert 'lang_name' in context
assert widget.lang_code == context["lang_code"] assert widget.lang_code == context['lang_code']
assert widget.lang_name == context["lang_name"] assert widget.lang_name == context['lang_name']
@staticmethod @staticmethod
def test_render(): def test_render():
"""Tests whether the :see:LocalizedFieldWidget correctly render.""" """Tests whether the :see:LocalizedFieldWidget correctly
render."""
widget = LocalizedFieldWidget() widget = LocalizedFieldWidget()
output = widget.render(name="title", value=None) output = widget.render(name='title', value=None)
assert bool(re.search(r"<label (.|\n|\t)*>\w+<\/label>", output)) assert bool(re.search('<label (.|\n|\t)*>\w+<\/label>', output))

14
tox.ini
View File

@ -1,20 +1,14 @@
[tox] [tox]
envlist = py36-dj{20,21,22,30,31,32}, py37-dj{20,21,22,30,31,32}, py38-dj{20,21,22,30,31,32,40,41,42}, py39-dj{30,31,32,40,41,42}, py310-dj{32,40,41,42,50}, py311-dj{42,50} envlist = py35-dj{111,20,21,22}, py36-dj{111,20,21,22}, py37-dj{111,20,21,22}
[testenv] [testenv]
deps = deps =
dj111: Django>=1.11,<1.12
dj20: Django>=2.0,<2.1 dj20: Django>=2.0,<2.1
dj21: Django>=2.1,<2.2 dj21: Django>=2.1,<2.2
dj22: Django>=2.2,<2.3 dj22: Django>=2.2,<2.3
dj30: Django>=3.0,<3.0.2 -rrequirements/test.txt
dj31: Django>=3.1,<3.2
dj32: Django>=3.2,<4.0
dj40: Django>=4.0,<4.1
dj41: Django>=4.1,<4.2
dj42: Django>=4.2,<5.0
dj50: Django>=5.0,<5.1
.[test]
setenv = setenv =
DJANGO_SETTINGS_MODULE=settings DJANGO_SETTINGS_MODULE=settings
passenv = DATABASE_URL passenv = DATABASE_URL
commands = python setup.py test commands = coverage run manage.py test