diff --git a/README.rst b/README.rst index 490305d..bf92714 100644 --- a/README.rst +++ b/README.rst @@ -242,12 +242,29 @@ Besides ``LocalizedField``, there's also: Experimental feature ^^^^^^^^^^^^^^^^^^^^ - Enables the following experimental features: - * ``LocalizedField`` will return ``None`` instead of an empty ``LocalizedValue`` if there is no database value. +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 + 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) diff --git a/localized_fields/admin.py b/localized_fields/admin.py new file mode 100644 index 0000000..fdb2227 --- /dev/null +++ b/localized_fields/admin.py @@ -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 diff --git a/localized_fields/forms.py b/localized_fields/forms.py index abc0ccd..2475673 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -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) diff --git a/localized_fields/static/localized_fields/localized-fields-admin.css b/localized_fields/static/localized_fields/localized-fields-admin.css new file mode 100644 index 0000000..df66ca6 --- /dev/null +++ b/localized_fields/static/localized_fields/localized-fields-admin.css @@ -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; +} diff --git a/localized_fields/static/localized_fields/localized-fields-admin.js b/localized_fields/static/localized_fields/localized-fields-admin.js new file mode 100644 index 0000000..9f4524b --- /dev/null +++ b/localized_fields/static/localized_fields/localized-fields-admin.js @@ -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) diff --git a/localized_fields/templates/localized_fields/admin/widget.html b/localized_fields/templates/localized_fields/admin/widget.html new file mode 100644 index 0000000..2d9152e --- /dev/null +++ b/localized_fields/templates/localized_fields/admin/widget.html @@ -0,0 +1,14 @@ +
diff --git a/localized_fields/widgets.py b/localized_fields/widgets.py new file mode 100644 index 0000000..36e9280 --- /dev/null +++ b/localized_fields/widgets.py @@ -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