mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-04-25 11:42:54 +03:00
Merge remote-tracking branch 'beer/admin_integration'
This commit is contained in:
commit
23c6f975d8
21
README.rst
21
README.rst
@ -242,14 +242,31 @@ Besides ``LocalizedField``, there's also:
|
|||||||
|
|
||||||
Experimental feature
|
Experimental feature
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
Enables the following experimental features:
|
Enables the following experimental features:
|
||||||
* ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value.
|
* ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
LOCALIZED_FIELDS_EXPERIMENTAL = True
|
LOCALIZED_FIELDS_EXPERIMENTAL = True
|
||||||
|
|
||||||
|
|
||||||
|
Django Admin Integration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
To enable widgets in the admin, you need to inherit from ``LocalizedFieldsAdminMixin``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from myapp.models import MyLocalizedModel
|
||||||
|
|
||||||
|
from localized_fields.admin import LocalizedFieldsAdminMixin
|
||||||
|
|
||||||
|
class MyLocalizedModelAdmin(LocalizedFieldsAdminMixin, admin.ModelAdmin):
|
||||||
|
"""Any admin options you need go here"""
|
||||||
|
|
||||||
|
admin.site.register(MyLocalizedModel, MyLocalizedModelAdmin)
|
||||||
|
|
||||||
|
|
||||||
Frequently asked questions (FAQ)
|
Frequently asked questions (FAQ)
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
27
localized_fields/admin.py
Normal file
27
localized_fields/admin.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from django.contrib.admin import ModelAdmin
|
||||||
|
|
||||||
|
from .fields import LocalizedField
|
||||||
|
from . import widgets
|
||||||
|
|
||||||
|
|
||||||
|
FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = {
|
||||||
|
LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedFieldsAdminMixin(ModelAdmin):
|
||||||
|
class Media:
|
||||||
|
css = {
|
||||||
|
'all': (
|
||||||
|
'localized_fields/localized-fields-admin.css',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
js = (
|
||||||
|
'localized_fields/localized-fields-admin.js',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs)
|
||||||
|
overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy()
|
||||||
|
overrides.update(self.formfield_overrides)
|
||||||
|
self.formfield_overrides = overrides
|
@ -2,53 +2,18 @@ from typing import List
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms import MultiWidget
|
|
||||||
|
|
||||||
from .localized_value import LocalizedValue
|
from .localized_value import LocalizedValue
|
||||||
|
from .widgets import LocalizedFieldWidget
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldWidget(MultiWidget):
|
|
||||||
"""Widget that has an input box for every language."""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
|
||||||
|
|
||||||
widgets = []
|
|
||||||
|
|
||||||
for _ in settings.LANGUAGES:
|
|
||||||
widgets.append(forms.Textarea())
|
|
||||||
|
|
||||||
super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs)
|
|
||||||
|
|
||||||
def decompress(self, value: LocalizedValue) -> List[str]:
|
|
||||||
"""Decompresses the specified value so
|
|
||||||
it can be spread over the internal widgets.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
value:
|
|
||||||
The :see:LocalizedValue to display in this
|
|
||||||
widget.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
All values to display in the inner widgets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for lang_code, _ in settings.LANGUAGES:
|
|
||||||
if value:
|
|
||||||
result.append(value.get(lang_code))
|
|
||||||
else:
|
|
||||||
result.append(None)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class LocalizedFieldForm(forms.MultiValueField):
|
class LocalizedFieldForm(forms.MultiValueField):
|
||||||
"""Form for a localized field, allows editing
|
"""Form for a localized field, allows editing
|
||||||
the field in multiple languages."""
|
the field in multiple languages."""
|
||||||
|
|
||||||
widget = LocalizedFieldWidget()
|
widget = LocalizedFieldWidget
|
||||||
|
value_class = LocalizedValue
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initializes a new instance of :see:LocalizedFieldForm."""
|
"""Initializes a new instance of :see:LocalizedFieldForm."""
|
||||||
@ -59,17 +24,21 @@ class LocalizedFieldForm(forms.MultiValueField):
|
|||||||
field_options = {'required': False}
|
field_options = {'required': False}
|
||||||
|
|
||||||
if lang_code == settings.LANGUAGE_CODE:
|
if lang_code == settings.LANGUAGE_CODE:
|
||||||
field_options['required'] = True
|
field_options['required'] = kwargs.get('required', True)
|
||||||
|
|
||||||
field_options['label'] = lang_code
|
field_options['label'] = lang_code
|
||||||
fields.append(forms.fields.CharField(**field_options))
|
fields.append(forms.fields.CharField(**field_options))
|
||||||
|
|
||||||
super(LocalizedFieldForm, self).__init__(
|
super(LocalizedFieldForm, self).__init__(
|
||||||
fields,
|
fields,
|
||||||
require_all_fields=False
|
require_all_fields=False,
|
||||||
|
*args, **kwargs
|
||||||
)
|
)
|
||||||
|
# set 'required' attribute for each widget separately
|
||||||
|
for f, w in zip(self.fields, self.widget.widgets):
|
||||||
|
w.is_required = f.required
|
||||||
|
|
||||||
def compress(self, value: List[str]) -> LocalizedValue:
|
def compress(self, value: List[str]) -> value_class:
|
||||||
"""Compresses the values from individual fields
|
"""Compresses the values from individual fields
|
||||||
into a single :see:LocalizedValue instance.
|
into a single :see:LocalizedValue instance.
|
||||||
|
|
||||||
@ -82,7 +51,7 @@ class LocalizedFieldForm(forms.MultiValueField):
|
|||||||
the value in several languages.
|
the value in several languages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
localized_value = LocalizedValue()
|
localized_value = self.value_class()
|
||||||
|
|
||||||
for (lang_code, _), value in zip(settings.LANGUAGES, value):
|
for (lang_code, _), value in zip(settings.LANGUAGES, value):
|
||||||
localized_value.set(lang_code, value)
|
localized_value.set(lang_code, value)
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
.localized-fields-widget {
|
||||||
|
margin-left: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
border: 1px solid #79aec8;
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background: #79aec8;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab:hover {
|
||||||
|
background: #417690;
|
||||||
|
border-color: #417690;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab a {
|
||||||
|
padding: 5px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab.active,
|
||||||
|
.localized-fields-widget.tabs .localized-fields-widget.tab.active:hover {
|
||||||
|
background: #79aec8;
|
||||||
|
border-color: #79aec8;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
(function($) {
|
||||||
|
var syncTabs = function(lang) {
|
||||||
|
$('.localized-fields-widget.tab a:contains("'+lang+'")').each(function(){
|
||||||
|
$(this).parents('.localized-fields-widget[role="tabs"]').find('.localized-fields-widget.tab').removeClass('active');
|
||||||
|
$(this).parents('.localized-fields-widget.tab').addClass('active');
|
||||||
|
$(this).parents('.localized-fields-widget[role="tabs"]').children('.localized-fields-widget [role="tabpanel"]').hide();
|
||||||
|
$($(this).attr('href')).show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function (){
|
||||||
|
$('.localized-fields-widget [role="tabpanel"]').hide();
|
||||||
|
// set first tab as active
|
||||||
|
$('.localized-fields-widget[role="tabs"]').each(function () {
|
||||||
|
$(this).find('.localized-fields-widget.tab:first').addClass('active');
|
||||||
|
$($(this).find('.localized-fields-widget.tab:first a').attr('href')).show();
|
||||||
|
});
|
||||||
|
// try set active last selected tab
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
var lang = window.sessionStorage.getItem('localized-field-lang');
|
||||||
|
if (lang) {
|
||||||
|
syncTabs(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.localized-fields-widget.tab a').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
syncTabs(this.innerText);
|
||||||
|
if (window.sessionStorage) {
|
||||||
|
window.sessionStorage.setItem('localized-field-lang', this.innerText);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(django.jQuery)
|
@ -0,0 +1,14 @@
|
|||||||
|
<div class="localized-fields-widget" role="tabs" data-synctabs="translation">
|
||||||
|
<ul class="localized-fields-widget tabs">
|
||||||
|
{% for key, lang in available_languages %}
|
||||||
|
<li class="localized-fields-widget tab">
|
||||||
|
<a href="#{{ id }}_{{ key }}">{{ lang|capfirst }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% for key, widget in widgets %}
|
||||||
|
<div role="tabpanel" id="{{ id }}_{{ key }}">
|
||||||
|
{{ widget }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
86
localized_fields/widgets.py
Normal file
86
localized_fields/widgets.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.admin import widgets
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
from .localized_value import LocalizedValue
|
||||||
|
|
||||||
|
|
||||||
|
class LocalizedFieldWidget(forms.MultiWidget):
|
||||||
|
"""Widget that has an input box for every language."""
|
||||||
|
widget = forms.Textarea
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initializes a new instance of :see:LocalizedFieldWidget."""
|
||||||
|
|
||||||
|
widgets = []
|
||||||
|
|
||||||
|
for _ in settings.LANGUAGES:
|
||||||
|
widgets.append(self.widget)
|
||||||
|
|
||||||
|
super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs)
|
||||||
|
|
||||||
|
def decompress(self, value: LocalizedValue) -> List[str]:
|
||||||
|
"""Decompresses the specified value so
|
||||||
|
it can be spread over the internal widgets.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
value:
|
||||||
|
The :see:LocalizedValue to display in this
|
||||||
|
widget.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
All values to display in the inner widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for lang_code, _ in settings.LANGUAGES:
|
||||||
|
if value:
|
||||||
|
result.append(value.get(lang_code))
|
||||||
|
else:
|
||||||
|
result.append(None)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class AdminLocalizedFieldWidget(LocalizedFieldWidget):
|
||||||
|
widget = widgets.AdminTextareaWidget
|
||||||
|
template = 'localized_fields/admin/widget.html'
|
||||||
|
|
||||||
|
def render(self, name, value, attrs=None):
|
||||||
|
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)
|
||||||
|
output = []
|
||||||
|
final_attrs = self.build_attrs(attrs)
|
||||||
|
id_ = final_attrs.get('id')
|
||||||
|
for i, widget in enumerate(self.widgets):
|
||||||
|
try:
|
||||||
|
widget_value = value[i]
|
||||||
|
except IndexError:
|
||||||
|
widget_value = None
|
||||||
|
if id_:
|
||||||
|
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
|
||||||
|
widget_attrs = self.build_widget_attrs(widget, widget_value, final_attrs)
|
||||||
|
output.append(widget.render(name + '_%s' % i, widget_value, widget_attrs))
|
||||||
|
context = {
|
||||||
|
'id': final_attrs.get('id'),
|
||||||
|
'name': name,
|
||||||
|
'widgets': zip([code for code, lang in settings.LANGUAGES], output),
|
||||||
|
'available_languages': settings.LANGUAGES
|
||||||
|
}
|
||||||
|
return render_to_string(self.template, context)
|
||||||
|
|
||||||
|
def build_widget_attrs(self, 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…
x
Reference in New Issue
Block a user