Compare commits

...

7 commits

Author SHA1 Message Date
3fa90b2e95 Merge branch 'translatable-fields' 2020-02-16 13:22:26 +01:00
4b861162d0 Format code with black 2020-02-16 13:15:17 +01:00
0240564eb5 Move project related files to app 2020-02-16 13:14:28 +01:00
de9362ff4c Update .gitignore 2020-02-16 13:12:26 +01:00
Denis Kildishev
fd2fad5605
Merge pull request #1 from geex-arts/dev
Update README.rst
2018-03-08 18:29:48 +03:00
Denis Kildishev
4fd09bf2de
Update README.rst 2018-03-08 18:29:22 +03:00
Denis K
fa9b67f657 Initial commit 2018-03-08 18:22:58 +03:00
11 changed files with 394 additions and 2 deletions

10
.gitignore vendored
View file

@ -1,6 +1,12 @@
.idea
*.sqlite3
result
*.pyc
*.pyo
__pycache__
*.DS_Store
*.egg*
dist/
docs/_build/
.idea/
node_modules/
result

View file

@ -0,0 +1,7 @@
Changelog
=========
0.0.1
-----
* Initial release

View 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.

View 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

View file

@ -0,0 +1 @@
VERSION = "0.0.1"

View 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

View 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;
}

View 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);

View 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>

View 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 ""

View 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