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
|
||||
result
|
||||
*.pyc
|
||||
*.pyo
|
||||
__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