Git Product home page Git Product logo

django-parler's Introduction

image

image

image

image

image

django-parler

Simple Django model translations without nasty hacks.

Features:

  • Nice admin integration.
  • Access translated attributes like regular attributes.
  • Automatic fallback to the default language.
  • Separate table for translated fields, compatible with django-hvad.
  • Plays nice with others, compatible with django-polymorphic, django-mptt and such:
    • No ORM query hacks.
    • Easy to combine with custom Manager or QuerySet classes.
    • Easy to construct the translations model manually when needed.

See the documentation for more details.

A brief overview

Installing django-parler

The package can be installed using:

pip install django-parler

Add the following settings:

INSTALLED_APPS += (
    'parler',
)

Optionally, the admin tabs can be configured too:

PARLER_LANGUAGES = {
    None: (
        {'code': 'en',},
        {'code': 'en-us',},
        {'code': 'it',},
        {'code': 'nl',},
    ),
    'default': {
        'fallback': 'en',             # defaults to PARLER_DEFAULT_LANGUAGE_CODE
        'hide_untranslated': False,   # the default; let .active_translations() return fallbacks too.
    }
}

Replace None with the SITE_ID when you run a multi-site project with the sites framework. Each SITE_ID can be added as additional entry in the dictionary.

Make sure your project is configured for multiple languages. It might be useful to limit the LANGUAGES setting. For example:

from django.utils.translation import gettext_lazy as _

LANGUAGE_CODE = 'en'

LANGUAGES = (
    ('en', _("English")),
    ('en-us', _("US English")),
    ('it', _('Italian')),
    ('nl', _('Dutch')),
    ('fr', _('French')),
    ('es', _('Spanish')),
)

By default, the fallback language is the same as LANGUAGE_CODE. The fallback language can be changed in the settings:

PARLER_DEFAULT_LANGUAGE_CODE = 'en'

Creating models

Using the TranslatedFields wrapper, model fields can be marked as translatable:

from django.db import models
from parler.models import TranslatableModel, TranslatedFields

class MyModel(TranslatableModel):
    translations = TranslatedFields(
        title = models.CharField(_("Title"), max_length=200)
    )

    def __unicode__(self):
        return self.title

Accessing fields

Translatable fields can be used like regular fields:

>>> object = MyModel.objects.all()[0]
>>> object.get_current_language()
'en'
>>> object.title
u'cheese omelet'

>>> object.set_current_language('fr')       # Only switches
>>> object.title = "omelette du fromage"    # Translation is created on demand.
>>> object.save()

Internally, django-parler stores the translated fields in a separate model, with one row per language.

Filtering translations

To query translated fields, use the .translated() method:

MyObject.objects.translated(title='cheese omelet')

To access objects in both the current and possibly the fallback language, use:

MyObject.objects.active_translations(title='cheese omelet')

This returns objects in the languages which are considered "active", which are:

  • The current language
  • The fallback language when hide_untranslated=False in the PARLER_LANGUAGES setting.

Changing the language

The queryset can be instructed to return objects in a specific language:

>>> objects = MyModel.objects.language('fr').all()
>>> objects[0].title
u'omelette du fromage'

This only sets the language of the object. By default, the current Django language is used.

Use object.get_current_language() and object.set_current_language() to change the language on individual objects. There is a context manager to do this temporary:

from parler.utils.context import switch_language

with switch_language(model, 'fr'):
    print model.title

And a function to query just a specific field:

model.safe_translation_getter('title', language_code='fr')

Advanced Features

This package also includes:

  • Creating the TranslatedFieldsModel manually!
  • Form classes for inline support.
  • View classes for switching languages, creating/updating translatable objects.
  • Template tags for language switching-buttons.
  • ORM methods to handle the translated fields.
  • Admin inlines support.

See the documentation for more details.

Special notes

  • Using ModelAdmin.prepopulated_fields doesn't work, but you can use get_prepopulated_fields() as workaround.
  • Due to ORM restrictions queries for translated fields should be performed in a single .translated(..) or .active_translations(..) call.
  • The .active_translations(..) method typically needs to .distinct() call to avoid duplicate results of the same object.

TODO

  • The list code currently performs one query per object. This needs to be reduced.
  • Preferably, the TranslatedField proxy on the model should behave like a RelatedField, if that would nicely with the ORM too.

Please contribute your improvements or work on these area's!

Contributing

This module is designed to be generic. In case there is anything you didn't like about it, or think it's not flexible enough, please let us know. We'd love to improve it!

If you have any other valuable contribution, suggestion or idea, please let us know as well because we will look into it. Pull requests are welcome too. :-)

django-parler's People

Contributors

bashu avatar bikeshedder avatar chive avatar dependabot[bot] avatar djw avatar dkopitsa avatar gurch101 avatar heavenshell avatar hedleyroos avatar imposeren avatar jmelett avatar jrief avatar martinsvoboda avatar maxg0 avatar mireq avatar mkoistinen avatar notsqrt avatar rsenk330 avatar simonline avatar skirsdeda avatar spapanik avatar stefanw avatar steinrobert avatar thedrow avatar timgates42 avatar vdboor avatar vstoykov avatar xavfernandez avatar xi avatar yakky avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-parler's Issues

Ambiguity in your documentation

https://github.com/edoburu/django-parler/blob/master/docs/configuration.rst

The section PARLER_ENABLE_CACHING (Line 110 I believe) states:

If needed, caching can be disabled. This is likely not needed.

I find the latter sentence confusing. Are you stating that the configuration line PARLER_ENABLE_CACHING = True is probably not needed, or that it is likely that one doesn't want to disable caching?

You might argue that my English comprehension is lacking, which I will not disagree. However, in my opinion documentation should be as unambiguous as possible and assuming that most people using your django app are non-native English speakers, you might want to communicate this point differently.

Error deleting translation

In django admin when deleting translation following error occurs (delete-translation/de/):
type object 'WysiwygWidgetFormFormSet' has no attribute 'fk'
Location: parler/admin.py in _get_inline_translations, line 503

Problem is with formset pk which doesn't exist:
fk = inline.get_formset(request, obj).fk
rel_name = 'master__{0}'.format(fk.name)

Do we actually need rel_name filter?

Parler + mptt does not work in admin

models.py

from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from parler.models import TranslatableModel, TranslatedFields

class Category(MPTTModel, TranslatableModel):
    translations = TranslatedFields(
        name = models.TextField(unique=True)
    )
    order_num = models.IntegerField(default=10)
    parent = TreeForeignKey('self',
        null=True, blank=True, related_name='children')

    def __unicode__(self):
        return self.name

admin.py

from parler.admin import TranslatableAdmin
from mptt.admin import MPTTModelAdmin
from .models import Category

class CategoryAdmin(MPTTModelAdmin, TranslatableAdmin):
    pass
admin.site.register(Category, CategoryAdmin)

When I come to admin and try to open Categories list I get an error:

Request Method: GET
Request URL: http://127.0.0.1:8000/admin/articles/category/

Django Version: 1.7.2
Python Version: 2.7.6
Installed Applications:
('django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'parler',
 'articles')
Installed Middleware:
('django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.middleware.locale.LocaleMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware')


Traceback:
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
  111.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/contrib/admin/options.py" in wrapper
  583.                 return self.admin_site.admin_view(view)(*args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/views/decorators/cache.py" in _wrapped_view_func
  52.         response = view_func(request, *args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/contrib/admin/sites.py" in inner
  206.             return view(request, *args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapper
  29.             return bound_func(*args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/utils/decorators.py" in bound_func
  25.                 return func.__get__(self, type(self))(*args2, **kwargs2)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/contrib/admin/options.py" in changelist_view
  1485.                 self.list_max_show_all, self.list_editable, self)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django/contrib/admin/views/main.py" in __init__
  75.         self.root_queryset = model_admin.get_queryset(request)
File "/home/alex/.virtualenvs/tests/local/lib/python2.7/site-packages/django_parler-1.2.1-py2.7.egg/parler/admin.py" in get_queryset
  158.                 raise ImproperlyConfigured("{0} class does not inherit from TranslatableQuerySet".format(qs.__class__.__name__))

Exception Type: ImproperlyConfigured at /admin/articles/category/
Exception Value: QuerySet class does not inherit from TranslatableQuerySet

Bug with inheritance?

Using parler 1.0

Hi again,

I have a model A with a translation table, and another model B that inherits from A, but with more translated fields.

I came up with this:

# -*- coding: utf-8 -*-
from django.db import models
from parler import models as parler_models


class MyModel(parler_models.TranslatableModel):
    title = parler_models.TranslatedField()


class MyModelTranslation(parler_models.TranslatedFieldsModel):
    master = models.ForeignKey(MyModel, related_name='translations', null=True)
    title = models.CharField(max_length=200)


class MyRicherModel(MyModel):
    description = parler_models.TranslatedField()


class MyRicherModelTranslation(parler_models.TranslatedFieldsModel):
    master = models.ForeignKey(MyRicherModel, related_name='translation', null=True)
    title = models.CharField(max_length=200)
    description = models.TextField()

But the model B is already registered to the model A's translation table, according to parler:

$ ./manage.py syncdb --noinput
No handlers could be found for logger "parler.models"
Traceback (most recent call last):
  File "./manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/__init__.py", line 399, in execute_from_command_line
    utility.execute()
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/__init__.py", line 392, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/base.py", line 242, in run_from_argv
    self.execute(*args, **options.__dict__)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/base.py", line 284, in execute
    self.validate()
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/base.py", line 310, in validate
    num_errors = get_validation_errors(s, app)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/core/management/validation.py", line 34, in get_validation_errors
    for (app_name, error) in get_app_errors().items():
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/db/models/loading.py", line 196, in get_app_errors
    self._populate()
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/db/models/loading.py", line 75, in _populate
    self.load_app(app_name, True)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/db/models/loading.py", line 99, in load_app
    models = import_module('%s.models' % app_name)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/django/utils/importlib.py", line 40, in import_module
    __import__(name)
  File "/Users/herve/dev/testcaseparlerinheritance/testcaseparlerinheritance/models.py", line 22, in <module>
    class MyRicherModelTranslation(parler_models.TranslatedFieldsModel):
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/parler/models.py", line 550, in __new__
    shared_model = _validate_master(new_class)
  File "/Users/herve/.virtualenvs/testcase-parler-inheritance/lib/python2.7/site-packages/parler/models.py", line 577, in _validate_master
    raise TypeError(msg)
TypeError: The model 'MyRicherModel' already has an associated translation table!

(This is a minimal app to reproduce the issue, the actual app even has a third model.)

Is it a known limitation of parler?

I consider writing a single translation model with all the fields combined to bypass this. Parler will just query the fields it is looking for.

"Adding translated fields to an existing model" in docs needs clarification

First of all, thanks for the nice package.

I'm having trouble to make sense of this code in the docs' section with the title above:

class TranslatableSite(TranslatableModel, Site):
    class Meta:
        proxy = True

    translations = TranslatedFields()

What exactly should be wrapped in TranslatedFields()?

On the same subject, I found a gist on integrating django-oscar with django-parler -- a good example of tweaking existing models for translation:

class Product(AbstractProduct, TranslatableModel):
    """
    Add translations to the product model.
    """

    # Provide translated fields.
    # Consider renaming tr_title to `title` and see if you can replace the original title field this way.
    translations = TranslatedFields(
        tr_title = models.CharField(_('Translated Title'), max_length=255, blank=True, null=True, help_text=_(u"The title can be highlighted by <strong>*surrounding words*</strong> with an asterisk.")),
        description = models.TextField(_('Translated Description'), blank=True, null=True),
    )

In this case the contents of TranslatedFields is provided; but then, having to redefine a field in the untranslated model and/or rename it (tr_title) strikes me as less than ideal. Am I missing something? Could parler provide a function that moves fields across classes to avoid field redefinition:

class Product(AbstractProduct, TranslatableModel):
    ...
     translations = TranslatedFields(
        title = move_field(AbstractProduct, 'title'),
        description = move_field(AbstractProduct, 'description'),
    )

Not sure if this is possible given model metaclass semantics in Django though.

thanks in advance,
Kaleb

No fallback with safe_translation_getter on a language different than current

In [4]: from django.conf import settings

In [5]: settings.PARLER_LANGUAGES
Out[5]: 
{2: ({'code': 'fr', 'fallback': 'fr', 'hide_untranslated': False},
  {'code': 'en', 'fallback': 'fr', 'hide_untranslated': False}),
 'default': {'code': 'fr', 'fallback': 'fr', 'hide_untranslated': False}}

In [6]: settings.SITE_ID
Out[6]: 2

In [7]: mv.safe_translation_getter('text_field', language_code='fr')
Out[7]: u'text fr'

In [8]: mv.safe_translation_getter('text_field', language_code='en')

In [9]: mv.set_current_language('en')

In [10]: mv.safe_translation_getter('text_field', language_code='en')
Out[10]: u'text fr'

The issue in my case is that https://github.com/edoburu/django-parler/blob/master/parler/models.py#L651 can raise TranslationDoesNotExist (and does) without trying to fallback on fr.

Integration with django-admin-sortable2

I'm using Admin sortable 2 in admin to order models by dragging them around. Parler works well with SortableAdminMixin but it doesn't work with SortableInlineAdminMixin. So basically, if the model is both translatable and inline, the two apps are not compatible. Just wonder if there's any tip or plan on integrating Admin sortable 2's SortableInlineAdminMixin.

Saving a proxy model instance saves also current active language

If - say - Portuguese is my currently set language, i.e. that I'm in /pt/admin/app/translatable-model/123/?language=de and I add or edit the German version, the Portuguese version is also overwritten. I have no idea if this behaviour is linked to the change, but it only happens on proxy models. Normal model inheritors of TranslatableModel have no problems under the same setup.

class ModelWithoutTranslations(models.Model):

    original_field = models.CharField(
        default="untranslated",
        max_length=255,
    )


class ExistingModelWithTranslations(TranslatableModel, ModelWithoutTranslations):

    translations = TranslatedFields(
        original_field=models.CharField(default="translated", max_length=255)
    )

    class Meta:
        proxy = True

The following test case proves the error:

def test_proxy_model_saving(self):
    """
    This test checks that contents of proxy models are correctly saved
    and do not affect other models or instances, see #41
    """

    translation.activate(translation.get_language())
    x = ModelWithoutTranslations(original_field="lalala")
    x.save()
    x = ExistingModelWithTranslations.objects.get(original_field="lalala")
    x.set_current_language(self.other_lang1)
    x.original_field = "test lang1"
    x.save()

    # Change the active language to other_lang1 and try to refetch the object
    translation.activate(self.other_lang1)
    x = ExistingModelWithTranslations.objects.get(original_field="test lang1")

    # Check that original is still untouched
    x = ModelWithoutTranslations.objects.get(original_field="lalala")

    translation.activate(translation.get_language())

Discussion about parler settings

I was recently thinking about the idea behind PARLER_LANGUAGES. As settings.SITE_ID and settings.PARLER_LANGUAGES are both rather static I wonder if it really makes sense that PARLER_LANGUAGES is a dict containing all sites.

If one really wants to keep the configuration of multiple sites in one settings file it would be really simple to do so without having parler to include any site logic:

SITE_ID = int(environ['DJANGO_SITE_ID'])
LANGUAGES = {
    1: [
        {'code': 'en'},
        {'code': 'fr'},
    ],
    2: [
        {'code': 'en'},
        {'code': 'de'},
        {'code': 'ru'},
    ]
}[settings.SITE_ID]

Personally I would use a separate settings_site1.py and settings_site2.py file which could import common settings from a settings_base.py, but this really is a matter of taste.


How about using the LANGUAGES by default for creating the admin tabs? A multi lingual site usually configures LANGUAGES anyways and just having to duplicate the language codes for parler should not be necessary if the user is happy with the per language defaults, no?


I also wonder why there is a PARLER_DEFAULT_LANGUAGE_CODE and PARLER_LANGUAGES['default']['fallback']. Why having two different ways of configuring things? Having a PARLER_DEFAULT_FALLBACK and PARLER_HIDE_UNTRANSLATED would make way more sense than having two styles of configuring things.


I'm a big fan of having one configuration dict per app (simmilar to DATABASES). For parler this could look like the following:

PARLER = {
    'DEFAULT_LANGUAGE_CODE': 'en',
    'DEFAULT_FALLBACK': 'en',
    'HIDE_UNTRANSLATED': False,
    'LANGUAGES': [
            'en': {},
            'en-us': {'FALLBACK': ['en']},
            'en-gb': {'FALLBACK': ['en']},
            'de: {}',
            'de-de': {'FALLBACK': ['de']},
            'de-at': {'FALLBACK': ['de']},
            'de-ch': {'FALLBACK': ['de']},
    ]
}

Now if someone really wants to have different settings per site it could simply be done by placing custom logic in the settings. Parler does not need to add explicit support for this to work.

If you want to have truly dynamic settings I would rather allow PARLER to point to a callable which can then create truly dynamic configuration 100% customizable by the user. In the end I really wonder if someone has such use case.


I might be missing an important point, but right now the parler configuration looks a bit clunky with the site specific settings mixed with a default key while other settings are direct attributes of the settings with a PARLER_ prefix.

For backwards compatibility PARLER_* settings could be deprecated in favor of the new PARLER setting.

Wish: be able to redefine existing fields as translatable

Based on the reports #38 and #39.

It's not possible to redefine fields as translatable if they also exist on the shared model.

  • The official procedure is adding the translatable fields, and then removing the fields from the shared model.
  • When overriding third-party models currently requires using different names for those fields (e.g. django-oscar, see https://gist.github.com/vdboor/46348490c7cd1a5b08f4).

The following could be improved:

  • Add detection that a field is being overwritten. Requires refactoring the metaclass, see #38 (comment)
  • Remove an existing field from the model Meta, implement an inverse of django.db.model.Options.add_field().
  • It makes sense to make the overriding explicit, e.g. title = TranslatedField(redefine_field=True)

django-parler with django-shop

Hi,
I find out that this should be the best package to use with django-shop. But I don't know what is necessary to make products attribute name multilingual? Is anywhere some example?

Thanks a lot.

Create form class to enter all languages at once.

This might be going a bit far, but would it be possible to switch between the languages without reloading the page? Ideally it would be nice for the user to be able to edit any of the different languages and hit "Save" and have them all save at once.

What do you think? Is this possible? Or very difficult?

Django Oscar demo at DjangoCon Europe?

Hello :-)
You gave nice demo during your presentation at the conference, would you please share how to start parler with Oscar or if you have any examples to start with? On gist or so would be great.

Best Regards,
Bashar

Repo description mistake

Hi,

The repo description says:
Easily translate "cheese omelet" into "omelette du fromage".

The "du" is wrong. It should be:
Easily translate "cheese omelet" into "omelette au fromage".

Cheers

How to switch language in admin

Hello.

As I tried to find solution, I was unable to find how to define/switch language during adding/editing of items in Admin. I'm using django-suit and was thinking that this was the cause but even after turning it of I didn't find any control/link to switch language as it was possible with django-hvad.

Am I missing something ?

Handling of ManyToMany fields is... missing?

Hey everyone.

I got a M2M field included in TranslatedFields to show up fairly easily on the shared model, by adding a call to cls._meta.get_m2m_with_model() in a couple of places. However, now I'm getting this error:

"<CardTranslation: #None, cy, master: #None>" needs to have a value for field
    "cardtranslation" before this many-to-many relationship can be used.

I kind of have an idea why this is happening and was considering taking it upon myself to implementing M2M support (depends whether my current project will end up needing it, I guess). I wanted to hear your thoughts on this first, though. I've only started reading the Parler code today -- maybe someone more experienced could share a few caveats?

Django 1.8 support?

I really want to use this package in my project, and I'm also hoping to use django 1.8 since this is a new project and 1.8 is the next LTS release due out in just a few days.

I get errors like this when trying to create a TranslatableModel in my app:

https://dpaste.de/oc54

Also, I noticed that the django.template.loader.find_template() function used in parler.template should be replaced with django.template.loader.get_template() for 1.8.

This will probably be where the problem is coming from:

https://docs.djangoproject.com/en/dev/ref/models/meta/#migrating-from-the-old-api

Fallback language not working with multiple fields

I might be doing something wrong here but it looks like the fallback doesn't work when the translation exists for a different field.
I have a model like:

class MyModel(TranslatableModel):
    title = TranslatedField(any_language=True)
    description = TranslatedField(any_language=True)
    translations = TranslatedFields(
        title=models.CharField(max_length=255),
        description=models.TextField(blank=True),
    )

If I create an object with a translation for both fields in 'en', then add a translated description for 'de', when I retrieve that object in 'de' I would expect to have the German description and the English title (as a fallback). What happens instead is that, since the German translation exists, I get the German description and an empty title.
I took a quick look at the tests and it appears this case is not covered, since they all seem to deal with a single "title" field. Of course, maybe I'm just doing something wrong, in which case please let me know.

_original_values in translation model should not contain master FK?

TranslatedFieldsModel does self._original_values = self._get_field_values() in the constructor, which causes an additional query to fetch master (if it's not in _master_cache, (which it can't be as the object hasn't been initialized yet...)).

This causes N queries when querying Translations by hand, which is not nice.

I think that _get_field_values should at the very least exclude master (master_id is enough, isn't it?) or maybe simply use get_translated_fields.

Am I missing something there?

translation not working...?

Hi

I find this is very interesting django module to work with, thanks for nice job...

I have an issue with my translation, being a new django user i am not able figure out where exactly i had gone wrong in my code... pls refer my code here... https://gist.github.com/gowram

I would like to know why translation not working when i change the language choice in my template page... is there some parts need to be done in my view or url...

Thanks & Regards
Rama

Deleting translation and incorrect breadcrumb

Deleting translation and broken breadcrumb, see screenshot
screen shot 2015-05-01 at 14 06 37

Home -> /admin/fluent_pages/ ( should be /admin/ )
Fluentpage -> /admin/fluent_pages/page/ ( should be /admin/fluent_pages/ )
Pages -> /admin/fluent_pages/page/1/ ( should be /admin/fluent_pages/page/)
Spanish: testa -> /admin/fluent_pages/1/delete-translation/ ( should be /admin/fluent_pages/page/1/ or /admin/fluent_pages/page/1/?language=es )

Non integer primary keys in TranslatableModel

For technical reasons the primary key of a translatable model needs to be a charfield, but when I try to retrieve the translation, the function get_translation_cache_key fails due to long(master_id)

There is some justification, to use long in this place? Any idea to solve this issue?

[Django 1.4] Edit in admin is broken

Steps:

  • Go and run the example app
  • go to the admin
  • Add or edit an article

Error is:

FieldError at /fr/admin/article/article/add/
Unknown field(s) (content, slug, title) specified for Article

The current code is:

class ArticleAdmin(TranslatableAdmin):

    # ...
    # declared_fieldsets works in django1.4, contrary to fieldsets
    declared_fieldsets = (
        (None, {
            'fields': ('title', 'slug', 'published', 'category'),
        }),
        ("Contents", {
            'fields': ('content',),
        })
    )

So for some reason, the admin can't find the translated fields.

Parler combined with Polymorphic and Django-1.7

Today I tried to upgrade to Django-1.7. Unfortunately this does not work, and I have no clue why. I then boiled my project down to a very simply Django installation and am still able to reproduce it.

My environment:
Django==1.7.1
django-parler==1.2.1
django-polymorphic==0.6
...and unimportant stuff...

in settings.py:

INSTALLED_APPS = (
    'polymorphic',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.admin',
    'django.contrib.staticfiles',
    'parler',
    'my_demo',
)

Then I used the demo model.py from the docs:

# -*- coding: utf-8 -*-
from django.db import models
from django.utils.encoding import python_2_unicode_compatible, force_text
from parler.models import TranslatableModel, TranslatedFields
from polymorphic import PolymorphicModel
from parler.managers import TranslatableManager

class Product(PolymorphicModel):
    code = models.CharField(blank=False, default='', max_length=16)
    price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00)

@python_2_unicode_compatible
class Pen(TranslatableModel, Product):
    default_manager = TranslatableManager()

    translations = TranslatedFields(
        identifier=models.CharField(blank=False, default='', max_length=255)
    )

    def __str__(self):
        return force_text(self.identifier)

but I'am not able to even bootstrap this demo app:

   (... snip - bootstrapping ...)
  File ".../django/db/models/base.py", line 299, in add_to_class
    value.contribute_to_class(cls, name)
  File ".../parler/models.py", line 195, in contribute_to_class
    create_translations_model(cls, name, self.meta, **self.fields)
  File ".../parler/models.py", line 158, in create_translations_model
    translations_model = TranslatedFieldsModelBase(name, (TranslatedFieldsModel,), attrs)
  File ".../parler/models.py", line 677, in __new__
    new_class = super(TranslatedFieldsModelBase, mcs).__new__(mcs, name, bases, attrs)
  File ".../django/db/models/base.py", line 170, in __new__
    new_class.add_to_class(obj_name, obj)
  File ".../django/db/models/base.py", line 299, in add_to_class
    value.contribute_to_class(cls, name)
  File ".../django/db/models/fields/related.py", line 1585, in contribute_to_class
    super(ForeignObject, self).contribute_to_class(cls, name, virtual_only=virtual_only)
  File ".../django/db/models/fields/related.py", line 272, in contribute_to_class
    add_lazy_relation(cls, self, other, resolve_related_class)
  File ".../django/db/models/fields/related.py", line 84, in add_lazy_relation
    operation(field, model, cls)
  File ".../django/db/models/fields/related.py", line 271, in resolve_related_class
    field.do_related_class(model, cls)
  File ".../django/db/models/fields/related.py", line 307, in do_related_class
    self.set_attributes_from_rel()
  File ".../django/db/models/fields/related.py", line 304, in set_attributes_from_rel
    self.rel.set_field_name()
  File ".../django/db/models/fields/related.py", line 1256, in set_field_name
    self.field_name = self.field_name or self.to._meta.pk.name
AttributeError: 'NoneType' object has no attribute 'name'

BTW, the same happens if I use the Book example.

I saw that polymorphic/query.py attempts to handle something:

        # We might assume that self.model._meta.pk.name gives us the name of the primary key field,
        # but it doesn't. Therefore we use polymorphic_primary_key_name, which we set up in base.py.

but adding a breakpoint into that method is never triggered.

If you want, I can put a reference implementation on GitHub.

unique_together on translated fields

unique_together parameter is not checked at the admin level with def validate_unique(), and it raises an IntegrityError when hit the database.

I defined the model as follows:

class SimpleModel(TranslatableModel):
    text  = models.CharField(_('name'), max_length=255),
    translations = TranslatedFields(
        name=models.CharField(_('name'), max_length=255),
        slug=models.SlugField(_('slug'), blank=True, db_index=True),
        meta={'unique_together': (('language_code', 'slug'),)}
    )

SQLite3, caching, parler 1.1

Hello,
I'm using python 2.7, Django 1.7 with an SQLite3 database and parler 1.1. I have written this small setup:

# testapp/models.py
class TestBlob(TranslatableModel):
    translations = TranslatedFields(
        name=django.db.models.CharField(max_length=10)
    )

# testapp/tests.py
class ParlerTest(TestCase):
    def test_a(self):
        testblob = TestBlob.objects.language("en").create(name="english")
        self.assertEqual("en", testblob.get_current_language())
        self.assertEqual(set(["en"]), set(testblob.get_available_languages()))
        with switch_language(testblob, "de"):
            testblob.name = "deutsch"
            testblob.save()
            self.assertEqual("de", testblob.get_current_language())
            self.assertEqual("deutsch", testblob.name)
            self.assertEqual(set(["en", "de"]), set(testblob.get_available_languages()))  # test fails here

The following error appears during testing:

...$ python manage.py test testapp.tests.ParlerTest.test_a testapp.tests.ParlerTest.test_a -v2
test_a (testapp.tests.ParlerTest) ... ok
test_a (testapp.tests.ParlerTest) ... FAIL

======================================================================
FAIL: test_a (testapp.tests.ParlerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "testapp/tests.py", line 23, in test_a
    self.assertEqual(set(["en", "de"]), set(testblob.get_available_languages()))
AssertionError: Items in the first set but not the second:
u'de'

----------------------------------------------------------------------
Ran 2 tests in 0.017s

The error does not seem to appear if I use parler 1.0 or use PostgreSQL or have PARLER_ENABLE_CACHING=False in my settings.

Can you please help me?

Any hints on how to mix Django-Polymorphic with Parler?

I am able to create translatable polymorphic models, by creating a model class which inherits from two classes TranslatableModel and another class derived from PolymorphicModel. If I create an admin interface for each type of model, the admin backend works fine.

django-polymorphic allows to create an admin interface which can handle more than one type of model. However, if I try to create an admin class which inherits from TranslatableAdmin and PolymorphicChildModelAdmin and I try to save the form, this form is rejected.

Is there any receipt on how to mix them?

Limiting the number of queries

Given this dummy model, I want to retrieve and display a list of these models performing only one query:

class Dummy(TranslatableModel):
    translations = TranslatedFields(
        a = models.CharField(max_length=50),
    )
    b = models.CharField(max_length=50)

Trying with the following code does not yield the expected results (N+1 queries):

In [2]: from django.utils import translation
In [3]: translation.activate('en')
In [4]: from blog.models import Dummy
In [5]: Dummy.objects.all()
(0.002) SELECT "blog_dummy"."id", "blog_dummy"."b" FROM "blog_dummy" LIMIT 21; args=()
(0.000) SELECT "blog_dummy_translation"."id", "blog_dummy_translation"."language_code", "blog_dummy_translation"."a", "blog_dummy_translation"."master_id" FROM "blog_dummy_translation" WHERE ("blog_dummy_translation"."master_id" = 1  AND "blog_dummy_translation"."language_code" = en ); args=(1, 'en')
(0.000) SELECT "blog_dummy_translation"."id", "blog_dummy_translation"."language_code", "blog_dummy_translation"."a", "blog_dummy_translation"."master_id" FROM "blog_dummy_translation" WHERE ("blog_dummy_translation"."master_id" = 2  AND "blog_dummy_translation"."language_code" = en ); args=(2, 'en')
Out[5]: [<Dummy: a1>, <Dummy: a2>]

The only way I found to do this, is querying directly the TranslatedModel:

In [6]: DummyTranslation=Dummy.translations.related.model
In [7]: DummyTranslation.objects.select_related('master').filter(language_code='en')
(0.000) SELECT "blog_dummy_translation"."id", "blog_dummy_translation"."language_code", "blog_dummy_translation"."a", "blog_dummy_translation"."master_id", "blog_dummy"."id", "blog_dummy"."b" FROM "blog_dummy_translation" LEFT OUTER JOIN "blog_dummy" ON ("blog_dummy_translation"."master_id" = "blog_dummy"."id") WHERE "blog_dummy_translation"."language_code" = en  LIMIT 21; args=('en',)
Out[7]: [<DummyTranslation: #1, en, master: #1>, <DummyTranslation: #2, en, master: #2>]

But then I have to access the shared model's fields through "master" (e.g. dummy.master.b).

Is there a better way to do this? What am I doing wrong?
Thanks

Best strategy for integrating with django-reversion

Let's say I have a model:

from parler.models import TranslatableModel, TranslatedFields
...

class House(TranslatableModel, models.Model):
    global_name = models.SlugField(max_length=255, db_index=True)
    translations = TranslatedFields(
        name=models.CharField(max_length=255),
        description=models.CharField(max_length=255, blank=True),
    )

Registering this model with Reversion is generally as easy as:

from reversion import VersionAdmin

class HouseAdmin(VersionAdmin):
    ...

This won't follow fk or m2m relations by default, so I have added this to a common app:

reversion.register(HouseTranslation)

and this to the models.py described above:

reversion.register(House, follow=["translations"])

which doesn't seem to restore properly. It is always displaying the most recent revision/versions no matter which historical revision you view. Via the command line, I can see that the correct data appears to be written to the revision, including a version for the parent model, and a version for the translated attributes model. Is this at all the correct way to do this, or is there some alternative method? I am looking into VersionAdapters to do this in a custom way, but want to see if there are simpler methods.

AttributeError when switching on caching

I can't explain why this happened, but I switched on caching and got this on the first try. It worked afterwards.

...
 File ".../parler/models.py", line 418, in _get_translated_model
   object = get_cached_translation(self, language_code, related_name=meta.rel_name, use_fallback=use_fallback)
 File ".../parler/cache.py", line 71, in get_cached_translation
   values = _get_cached_values(instance, translated_model, language_code, use_fallback)
 File ".../parler/cache.py", line 119, in _get_cached_values
   if values.get('__FALLBACK__', False):
AttributeError: 'str' object has no attribute 'get'

It looks like values can be a non-empty string at some point(?) before properly initialized in the cache(?). Feel free to close this, as I could not repeat it.

Django 1.7?

Hi Diederik,

It looks like parler does not conform Django 1.7 requirements, does it?
I've got the following error:
"(admin.E016) The value of 'form' must inherit from 'BaseModelForm'."

In your example ArticleAdminForm inherits TranslatableModelForm, which inherits forms.ModelForm, but not BaseModelForm..

Thanks
Iho

dump_data dumps unloadable fixtures for translated models

class ConnectionType(TranslatableModel):
    translations = TranslatedFields(
        # дома, квартиры...
        name=models.CharField(_(u'Name'), max_length=50),
    )

    slug = models.SlugField(
        _(u'Slug'),
        unique=True,
        blank=True,
        help_text=_(u"если не заполнить, то будет заполнено на основе названия")
    )

this will dump data with 'name' field which is not loadable

Rest Framework 3??

Hi,

How's the integration with the new version of Rest Framework?
The docs mention a TranslatableModelSerializer, but the parler.contrib package is empty...

The project seems really neat, but Rest Framework compatibility is a must for us.

Thanks!

support zero pk

Hi,

I started using this package and it's really awesome !
I found a problem that 0 pk are never supported.

The Exception "<class 'parler.models.DoesNotExist'>' will be raised.
with the message fomatted like '{1} does not have a translation for the current language!'

I'm not sure if this is by design or just a bug.
What solved me the problem (and what I suggest as fix is):

In models.py line 386 change from:
if not self._state.adding and self.pk:
to
if not self._state.adding and self.pk is not None:

what do you think?
cheers
Lior.

TranslatedField issues

I think that on row 138 of file forms.py the last parameter is meant to be **attrs[f_name].kwargs instead of **translated_fields[f_name].kwargs.

Besides that, I'm having issues getting the field rendered: I'd like to use a CKEditor widget, and that does not seem to work, but even if I remove the custom widget setting the label is rendered as %(contents)s: instead of simply contents.

edit: the label thing was totally a (stupid) error on my side, but I still can't get the custom widget to work.

Is there any way I could fix this?

edit: well disregard all of that, I am a tool.

Thanks

Using a language code as a parameter when creating a form

Would it be possible to use a language code parameter when building a TranslatableModelForm, so that the resulting form will display and save the translated fields, maybe creating them if they don't exist?
Or is the expected workflow to use with switch_language(model, lang): MForm(instance=model)?

Changing translations on proxy models fail

Using the pattern of existing models with multiple inheritance and Meta.proxy = True wrongly causes TranslatableModel to not fetch existing language instances when saving.

Fixed in #39 - plus test case.

The issue is that whenever a translated model made by a proxy is changed, the following happens:

step1

step2

Fundamental Bug

Guys, come on. Your tagline has a translation mistake

"Cheese Omelet" is not "Omelette du fromage".

In English, "Omelette du fromage" would be "Omelet of the Cheese".

You're looking for "Omelette au fromage".

Optional translation object not created in admin

Hello,
if all translation fields are only optional and user do not fulfill values in Django admin, translation object is not created. Is possible to force creation of object?

Example of model:

class Author(TranslatableModel):
    translations = TranslatedFields(
        name = models.ForeignKey('AuthorName', blank=True, null=True),
        description = models.TextField(blank=True),
        like_count = models.BigIntegerField(default=0),
        quote_count = models.BigIntegerField(default=0),
    )

Parler generates permissions longer than 50 chars

Using version 1.0.

Django's auth.Permission.name is limited to 50 chars, but parler can generate permission names longer than that.

For example with a model labelled "Terms and conditions", "Can add version of the Terms and conditions Translation" is 55 chars. Even 58 with "Can change" and "Can delete" permissions.

django-rest-framework pairing

Hi Diederik,

Thank you for your nice parler.
I know this is piece of work, but maybe you have intent to write Translatable Serializers for Django-Rest-Framework? It would be great..

Thanks

Iho

AttributeError: 'TranslatableDictionaryWord' object has no attribute '_current_language'

Proxy model for models with existing data raises AttributeError: 'TranslatableDictionaryWord' object has no attribute '_current_language'

Traceback (most recent call last):
  File "local/lib/python2.7/site-packages/django/core/handlers/base.py", line 113, in get_response
    response = callback(request, *callback_args, **callback_kwargs)
  File "local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 372, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
  File "local/lib/python2.7/site-packages/django/utils/decorators.py", line 91, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "local/lib/python2.7/site-packages/django/views/decorators/cache.py", line 89, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "local/lib/python2.7/site-packages/django/contrib/admin/sites.py", line 202, in inner
    return view(request, *args, **kwargs)
  File "local/lib/python2.7/site-packages/django/utils/decorators.py", line 25, in _wrapper
    return bound_func(*args, **kwargs)
  File "local/lib/python2.7/site-packages/django/utils/decorators.py", line 91, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "local/lib/python2.7/site-packages/django/utils/decorators.py", line 21, in bound_func
    return func(self, *args2, **kwargs2)
  File "local/lib/python2.7/site-packages/django/contrib/admin/options.py", line 1285, in changelist_view
    'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
  File "local/lib/python2.7/site-packages/django/db/models/query.py", line 106, in __len__
    self._result_cache = list(self.iterator())
  File "local/lib/python2.7/site-packages/parler/managers.py", line 103, in iterator
    for obj in base_iterator:
  File "local/lib/python2.7/site-packages/django/db/models/query.py", line 327, in iterator
    obj = model(*row_data)
  File "local/lib/python2.7/site-packages/parler/models.py", line 223, in __init__
    super(TranslatableModel, self).__init__(*args, **kwargs)
  File "local/lib/python2.7/site-packages/django/db/models/base.py", line 348, in __init__
    setattr(self, field.attname, val)
  File "local/lib/python2.7/site-packages/parler/fields.py", line 99, in __set__
    translation = instance._get_translated_model(use_fallback=False, auto_create=True)
  File "local/lib/python2.7/site-packages/parler/models.py", line 304, in _get_translated_model
    language_code = self._current_language
AttributeError: 'TranslatableDictionaryWord' object has no attribute '_current_language'

Sending PR to fix it shortly...

There appears to be a maximum recursion depth error.

At least when attempting to use the latest master branch of django-parler with django-fluent-pages.

Here's a traceback of the last few recursions.

File "/Users/tailee/.virtualenvs/fluent-demo/src/django-parler/parler/admin.py" in get_form
  205.         form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django/django/contrib/admin/options.py" in get_form
  503.             fields = flatten_fieldsets(self.get_fieldsets(request, obj))
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_fieldsets
  478.         other_fields = self.get_subclass_fields(request, obj)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_subclass_fields
  497.         form = self.get_form(request, obj, exclude=exclude)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_form
  412.         return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django-parler/parler/admin.py" in get_form
  205.         form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django/django/contrib/admin/options.py" in get_form
  503.             fields = flatten_fieldsets(self.get_fieldsets(request, obj))
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_fieldsets
  478.         other_fields = self.get_subclass_fields(request, obj)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_subclass_fields
  497.         form = self.get_form(request, obj, exclude=exclude)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_form
  412.         return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django-parler/parler/admin.py" in get_form
  205.         form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django/django/contrib/admin/options.py" in get_form
  503.             fields = flatten_fieldsets(self.get_fieldsets(request, obj))
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_fieldsets
  478.         other_fields = self.get_subclass_fields(request, obj)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_subclass_fields
  497.         form = self.get_form(request, obj, exclude=exclude)
File "/Users/tailee/.virtualenvs/fluent-demo/lib/python2.7/site-packages/polymorphic/admin.py" in get_form
  412.         return super(PolymorphicChildModelAdmin, self).get_form(request, obj, **kwargs)
File "/Users/tailee/.virtualenvs/fluent-demo/src/django-parler/parler/admin.py" in get_form
  205.         form_class = super(TranslatableAdmin, self).get_form(request, obj, **kwargs)

Exception Type: RuntimeError at /admin/fluent_pages/page/add/
Exception Value: maximum recursion depth exceeded in cmp

Model instances created in interactive shell or data migrations have wrong default language.

My default language is en-au according to Django and Parler settings. But objects created from the interactive shell or a data migration have a default language of en-us.

This caused much confusion as I logged into the admin and clicked through to edit the object I had created in my data migration and found that all of its translated fields were empty in the form.

But in the change list, it showed me the en-us translation, causing even more confusion.

>>> from django.conf import settings
>>> from django.contrib.auth import get_user_model
>>> from fluent_pages.models import PageLayout
>>> from fluent_pages.pagetypes.fluentpage.models import FluentPage
>>> from parler import appsettings

>>> User = get_user_model()

>>> settings.LANGUAGE_CODE
'en-au'

>>> appsettings.PARLER_DEFAULT_LANGUAGE_CODE
'en-au'

>>> admin = User.objects.get(username='admin')
>>> home = PageLayout.objects.get(key='home')

>>> page = FluentPage.objects.create(
    author=admin,
    title='Home Page',
    status='p',
    layout=home,
    override_url='/',
)

>>> page.get_current_language()
u'en-us'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.