Add sphinx based docs

This commit is contained in:
Swen Kooij 2019-10-20 17:11:33 +03:00
parent 7bf0311306
commit cdf1831d35
17 changed files with 839 additions and 0 deletions

1
docs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build/

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# 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)

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@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

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

10
docs/source/conf.py Normal file
View File

@ -0,0 +1,10 @@
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']

100
docs/source/constraints.rst Normal file
View File

@ -0,0 +1,100 @@
.. _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 is 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

@ -0,0 +1,19 @@
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

99
docs/source/fields.rst Normal file
View File

@ -0,0 +1,99 @@
.. _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.

44
docs/source/filtering.rst Normal file
View File

@ -0,0 +1,44 @@
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"})

22
docs/source/index.rst Normal file
View File

@ -0,0 +1,22 @@
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.7 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

@ -0,0 +1,73 @@
.. _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/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

@ -0,0 +1,79 @@
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')

59
docs/source/querying.rst Normal file
View File

@ -0,0 +1,59 @@
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'])

140
docs/source/quick_start.rst Normal file
View File

@ -0,0 +1,140 @@
.. _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

17
docs/source/releases.rst Normal file
View File

@ -0,0 +1,17 @@
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``.

82
docs/source/saving.rst Normal file
View File

@ -0,0 +1,82 @@
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

39
docs/source/settings.rst Normal file
View File

@ -0,0 +1,39 @@
.. _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
}