Compare commits
7 commits
824d04c65d
...
3fa90b2e95
| Author | SHA1 | Date | |
|---|---|---|---|
| 3fa90b2e95 | |||
| 4b861162d0 | |||
| 0240564eb5 | |||
| de9362ff4c | |||
|
|
fd2fad5605 |
||
|
|
4fd09bf2de |
||
|
|
fa9b67f657 |
11 changed files with 394 additions and 2 deletions
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -1,6 +1,12 @@
|
||||||
.idea
|
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
result
|
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
__pycache__
|
__pycache__
|
||||||
|
*.DS_Store
|
||||||
|
*.egg*
|
||||||
|
|
||||||
|
dist/
|
||||||
|
docs/_build/
|
||||||
|
.idea/
|
||||||
|
node_modules/
|
||||||
|
result
|
||||||
|
|
|
||||||
7
translatable_fields/CHANGELOG.rst
Normal file
7
translatable_fields/CHANGELOG.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
0.0.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Initial release
|
||||||
20
translatable_fields/LICENSE
Normal file
20
translatable_fields/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
Copyright (c) 2012 Selwin Ong
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
90
translatable_fields/README.rst
Normal file
90
translatable_fields/README.rst
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
==========================
|
||||||
|
Django Translatable Fields
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Translatable model fields for Django with admin integration. Uses PostgreSQL JSONField.
|
||||||
|
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
* Add application in `settings.py`
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
INSTALLED_APPS = (
|
||||||
|
...
|
||||||
|
'translatable_fields',
|
||||||
|
...
|
||||||
|
)
|
||||||
|
|
||||||
|
* Specify languages in `settings.py`
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en'
|
||||||
|
LANGUAGES = (
|
||||||
|
('en', 'English'),
|
||||||
|
('ru', 'Русский')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
* Add `TranslatableField` model fields
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from translatable_fields.models import TranslatableField
|
||||||
|
|
||||||
|
|
||||||
|
class Position(models.Model):
|
||||||
|
...
|
||||||
|
title = TranslatableField(
|
||||||
|
verbose_name=_('title')
|
||||||
|
)
|
||||||
|
description = TranslatableField(
|
||||||
|
verbose_name=_('description')
|
||||||
|
)
|
||||||
|
...
|
||||||
|
|
||||||
|
* Create custom model admin form
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.postgres.forms import JSONField
|
||||||
|
from ckeditor_uploader.widgets import CKEditorUploadingWidget
|
||||||
|
|
||||||
|
from careers.models.position import Position
|
||||||
|
from translatable_fields.widgets import TranslatableWidget
|
||||||
|
|
||||||
|
|
||||||
|
class PositionAdminForm(forms.ModelForm):
|
||||||
|
title = JSONField(widget=TranslatableWidget(widget=forms.TextInput))
|
||||||
|
description = JSONField(widget=TranslatableWidget(widget=CKEditorUploadingWidget))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Position
|
||||||
|
fields = (
|
||||||
|
...
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
...
|
||||||
|
)
|
||||||
|
|
||||||
|
* Create custom model admin with custom form
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from careers.forms.admin.position import PositionAdminForm
|
||||||
|
|
||||||
|
|
||||||
|
class PositionAdmin(admin.ModelAdmin):
|
||||||
|
form = PositionAdminForm
|
||||||
|
|
||||||
1
translatable_fields/__init__.py
Normal file
1
translatable_fields/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
VERSION = "0.0.1"
|
||||||
26
translatable_fields/models.py
Normal file
26
translatable_fields/models.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
|
|
||||||
|
from translatable_fields.value import TranslatableValue
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatableField(JSONField):
|
||||||
|
def from_db_value(self, value, *args, **kwargs):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
instance = TranslatableValue()
|
||||||
|
instance.update(value)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if isinstance(value, TranslatableValue):
|
||||||
|
return value
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
instance = TranslatableValue()
|
||||||
|
instance.update(value)
|
||||||
|
|
||||||
|
return instance
|
||||||
47
translatable_fields/static/translatable_fields/translatable_fields.css
Executable file
47
translatable_fields/static/translatable_fields/translatable_fields.css
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
.translatable_field {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs-item {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: #79aec8;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs-item:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs-item:hover {
|
||||||
|
background: #417690;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs-item label, .inline-group .translatable_field__tabs-item label {
|
||||||
|
width: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 0 1px 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field__tabs-item_active,
|
||||||
|
.translatable_field__tabs-item_active:hover {
|
||||||
|
background: #79aec8;
|
||||||
|
border-color: #79aec8;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.translatable_field p.file-upload {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
37
translatable_fields/static/translatable_fields/translatable_fields.js
Executable file
37
translatable_fields/static/translatable_fields/translatable_fields.js
Executable file
|
|
@ -0,0 +1,37 @@
|
||||||
|
(function($) {
|
||||||
|
var syncTabs = function($container, lang) {
|
||||||
|
$container.find('.translatable_field__tabs-item label:contains("'+lang+'")').each(function(){
|
||||||
|
$(this).parents('.translatable_field').find('.translatable_field__tabs-item').removeClass('translatable_field__tabs-item_active');
|
||||||
|
$(this).parents('.translatable_field__tabs-item').addClass('translatable_field__tabs-item_active');
|
||||||
|
$(this).parents('.translatable_field').find('.translatable_field__widgets-item').hide();
|
||||||
|
$('#'+$(this).attr('for')).parents('.translatable_field__widgets-item').show();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$(function (){
|
||||||
|
$('.translatable_field__widgets-item').hide();
|
||||||
|
// set first tab as active
|
||||||
|
$('.translatable_field').each(function () {
|
||||||
|
$(this).find('.translatable_field__tabs-item:first').addClass('translatable_field__tabs-item_active');
|
||||||
|
syncTabs($(this), $(this).find('.translatable_field__tabs-item:first label').text());
|
||||||
|
});
|
||||||
|
// try set active last selected tab
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
var lang = window.sessionStorage.getItem('translatable-field-lang');
|
||||||
|
if (lang) {
|
||||||
|
$('.translatable_field').each(function () {
|
||||||
|
syncTabs($(this), lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.translatable_field__tabs-item label').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
syncTabs($(this).parents('.translatable_field'), $(this).text());
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
window.sessionStorage.setItem('translatable-field-lang', $(this).text());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(django.jQuery);
|
||||||
17
translatable_fields/templates/translatable_fields/multiwidget.html
Executable file
17
translatable_fields/templates/translatable_fields/multiwidget.html
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="translatable_field">
|
||||||
|
<div class="translatable_field__tabs">
|
||||||
|
{% for widget in widget.subwidgets %}
|
||||||
|
<div class="translatable_field__tabs-item">
|
||||||
|
<label for="{{ widget.attrs.id }}">{{ widget.lang_name }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="translatable_field__widgets">
|
||||||
|
{% for widget in widget.subwidgets %}
|
||||||
|
<div class="translatable_field__widgets-item" id="{{ widget.attrs.id }}_{{ widget.lang_code }}">
|
||||||
|
{# {% include widget.template_name %}#}
|
||||||
|
{{ widget.html }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
translatable_fields/value.py
Normal file
18
translatable_fields/value.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import translation
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatableValue(dict):
|
||||||
|
def __str__(self):
|
||||||
|
language = translation.get_language() or settings.LANGUAGE_CODE
|
||||||
|
languages = [language]
|
||||||
|
|
||||||
|
if len(self):
|
||||||
|
languages.append(list(self.keys())[0])
|
||||||
|
|
||||||
|
for lang_code in languages:
|
||||||
|
value = self.get(lang_code)
|
||||||
|
if value:
|
||||||
|
return value or ""
|
||||||
|
|
||||||
|
return ""
|
||||||
123
translatable_fields/widgets.py
Normal file
123
translatable_fields/widgets.py
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
class TranslatableWidget(forms.MultiWidget):
|
||||||
|
template_name = "translatable_fields/multiwidget.html"
|
||||||
|
widget = forms.Textarea
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = (settings.STATIC_URL + "translatable_fields/translatable_fields.js",)
|
||||||
|
css = {
|
||||||
|
"all": (
|
||||||
|
settings.STATIC_URL + "translatable_fields/translatable_fields.css",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, widget=None, *args, **kwargs):
|
||||||
|
if widget:
|
||||||
|
self.widget = widget
|
||||||
|
|
||||||
|
initial_widgets = [self.widget for _ in settings.LANGUAGES]
|
||||||
|
|
||||||
|
super().__init__(initial_widgets, *args, **kwargs)
|
||||||
|
|
||||||
|
for ((lang_code, lang_name), sub_widget) in zip(
|
||||||
|
settings.LANGUAGES, self.widgets
|
||||||
|
):
|
||||||
|
sub_widget.attrs["lang"] = lang_code
|
||||||
|
sub_widget.lang_code = lang_code
|
||||||
|
sub_widget.lang_name = lang_name
|
||||||
|
|
||||||
|
def decompress(self, value):
|
||||||
|
"""
|
||||||
|
Returns a list of decompressed values for the given compressed value.
|
||||||
|
The given value can be assumed to be valid, but not necessarily
|
||||||
|
non-empty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
value = json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
value = {}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
if value:
|
||||||
|
result.append(value.get(lang_code))
|
||||||
|
else:
|
||||||
|
result.append(None)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
result = dict(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
widget.lang_code,
|
||||||
|
widget.value_from_datadict(data, files, name + "_%s" % i),
|
||||||
|
)
|
||||||
|
for i, widget in enumerate(self.widgets)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if all(map(lambda x: x == "", result.values())):
|
||||||
|
return ""
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
context = super(forms.MultiWidget, self).get_context(name, value, attrs)
|
||||||
|
if self.is_localized:
|
||||||
|
for widget in self.widgets:
|
||||||
|
widget.is_localized = self.is_localized
|
||||||
|
# value is a list of values, each corresponding to a widget
|
||||||
|
# in self.widgets.
|
||||||
|
if not isinstance(value, list):
|
||||||
|
value = self.decompress(value)
|
||||||
|
|
||||||
|
final_attrs = context["widget"]["attrs"]
|
||||||
|
input_type = final_attrs.pop("type", None)
|
||||||
|
id_ = final_attrs.get("id")
|
||||||
|
subwidgets = []
|
||||||
|
|
||||||
|
for i, widget in enumerate(self.widgets):
|
||||||
|
if input_type is not None:
|
||||||
|
widget.input_type = input_type
|
||||||
|
widget_name = "%s_%s" % (name, i)
|
||||||
|
try:
|
||||||
|
widget_value = value[i]
|
||||||
|
except IndexError:
|
||||||
|
widget_value = None
|
||||||
|
if id_:
|
||||||
|
widget_attrs = final_attrs.copy()
|
||||||
|
widget_attrs["id"] = "%s_%s" % (id_, i)
|
||||||
|
else:
|
||||||
|
widget_attrs = final_attrs
|
||||||
|
widget_attrs = self.build_widget_attrs(widget, widget_value, widget_attrs)
|
||||||
|
widget_context = widget.get_context(
|
||||||
|
widget_name, widget_value, widget_attrs
|
||||||
|
)["widget"]
|
||||||
|
widget_context.update(
|
||||||
|
dict(lang_code=widget.lang_code, lang_name=widget.lang_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
widget_context["html"] = widget.render(
|
||||||
|
widget_name, widget_value, widget_attrs
|
||||||
|
)
|
||||||
|
subwidgets.append(widget_context)
|
||||||
|
context["widget"]["subwidgets"] = subwidgets
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def build_widget_attrs(widget, value, attrs):
|
||||||
|
attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
|
||||||
|
|
||||||
|
if (
|
||||||
|
not widget.use_required_attribute(value) or not widget.is_required
|
||||||
|
) and "required" in attrs:
|
||||||
|
del attrs["required"]
|
||||||
|
|
||||||
|
return attrs
|
||||||
Loading…
Add table
Add a link
Reference in a new issue