mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-30 10:38:58 +03:00 
			
		
		
		
	Merge remote-tracking branch 'beer/admin_integration'
This commit is contained in:
		
							
								
								
									
										17
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.rst
									
									
									
									
									
								
							| @@ -250,6 +250,23 @@ Experimental feature | |||||||
|      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 | ||||||
		Reference in New Issue
	
	Block a user