Merge remote-tracking branch 'beer/admin_integration'

This commit is contained in:
Swen Kooij 2017-04-03 14:56:43 +03:00
commit 23c6f975d8
7 changed files with 241 additions and 46 deletions

View File

@ -242,14 +242,31 @@ Besides ``LocalizedField``, there's also:
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.
.. code-block:: python
.. code-block:: python
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)
--------------------------------

27
localized_fields/admin.py Normal file
View 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

View File

@ -2,53 +2,18 @@ from typing import List
from django import forms
from django.conf import settings
from django.forms import MultiWidget
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):
"""Form for a localized field, allows editing
the field in multiple languages."""
widget = LocalizedFieldWidget()
widget = LocalizedFieldWidget
value_class = LocalizedValue
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedFieldForm."""
@ -59,17 +24,21 @@ class LocalizedFieldForm(forms.MultiValueField):
field_options = {'required': False}
if lang_code == settings.LANGUAGE_CODE:
field_options['required'] = True
field_options['required'] = kwargs.get('required', True)
field_options['label'] = lang_code
fields.append(forms.fields.CharField(**field_options))
super(LocalizedFieldForm, self).__init__(
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
into a single :see:LocalizedValue instance.
@ -82,7 +51,7 @@ class LocalizedFieldForm(forms.MultiValueField):
the value in several languages.
"""
localized_value = LocalizedValue()
localized_value = self.value_class()
for (lang_code, _), value in zip(settings.LANGUAGES, value):
localized_value.set(lang_code, value)

View File

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

View File

@ -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)

View File

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

View 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