Make available languages configurable

This commit is contained in:
Raphael Michel
2015-04-06 00:14:59 +02:00
parent 95c31ec3f6
commit febfea68f7
8 changed files with 160 additions and 25 deletions

View File

@@ -1,11 +1,25 @@
from django.forms.models import ModelFormMetaclass, BaseModelForm from django.forms.models import ModelFormMetaclass, BaseModelForm
from django import forms from django import forms
from django.utils import six from django.utils import six
from pretix.base.i18n import I18nFormField
from versions.models import Versionable from versions.models import Versionable
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class VersionedBaseModelForm(BaseModelForm): class BaseI18nModelForm(BaseModelForm):
"""
This is a helperclass to construct I18nModelForm
"""
def __init__(self, *args, **kwargs):
event = kwargs.pop('event', None)
super().__init__(*args, **kwargs)
if event:
for k, field in self.fields.items():
if isinstance(field, I18nFormField):
field.widget.enabled_langcodes = event.settings.get('locales')
class VersionedBaseModelForm(BaseI18nModelForm):
""" """
This is a helperclass to construct VersionedModelForm This is a helperclass to construct VersionedModelForm
""" """
@@ -18,7 +32,7 @@ class VersionedBaseModelForm(BaseModelForm):
class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)): class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseModelForm)):
""" """
This is a modified version of Django's ModelForm which differs from ModelForm in This is a modified version of I18nModelForm which differs from I18nModelForm in
only one way: It executes the .clone() method of an object before saving it back to only one way: It executes the .clone() method of an object before saving it back to
the database, if the model is a sub-class of versions.models.Versionable. You can the database, if the model is a sub-class of versions.models.Versionable. You can
safely use this as a base class for all your model forms, it will work out correctly safely use this as a base class for all your model forms, it will work out correctly
@@ -27,6 +41,17 @@ class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseMod
pass pass
class I18nModelForm(six.with_metaclass(ModelFormMetaclass, BaseI18nModelForm)):
"""
This is a modified version of Django's ModelForm which differs from ModelForm in
only one way: The constructor takes one additional optional argument ``event``
which may be given an :pyclass:`pretix.base.models.Event` instance. If given, this
instance is used to select the visible languages in all I18nFormFields of the form. If
not given, all languages will be displayed.
"""
pass
class SettingsForm(forms.Form): class SettingsForm(forms.Form):
""" """
This form is meant to be used for modifying Event- or OrganizerSettings This form is meant to be used for modifying Event- or OrganizerSettings

View File

@@ -4,10 +4,18 @@ from django.conf import settings
from django.db.models import TextField, SubfieldBase from django.db.models import TextField, SubfieldBase
from django import forms from django import forms
from django.utils import translation from django.utils import translation
from django.utils.safestring import mark_safe
class LazyI18String: class LazyI18nString:
"""
This represents an internationalized string that is/was/will be stored in the database.
"""
def __init__(self, data): def __init__(self, data):
"""
Input data should be a dictionary which maps language codes to content.
"""
self.data = data self.data = data
if isinstance(self.data, str) and self.data is not None: if isinstance(self.data, str) and self.data is not None:
try: try:
@@ -18,6 +26,11 @@ class LazyI18String:
self.data = j self.data = j
def __str__(self): def __str__(self):
"""
Evaluate the given string with respect to the currently active locale.
This will rather return you a string in a wrong language than give you an
empty value.
"""
if self.data is None: if self.data is None:
return "" return ""
if isinstance(self.data, dict): if isinstance(self.data, dict):
@@ -41,14 +54,19 @@ class LazyI18String:
class I18nWidget(forms.MultiWidget): class I18nWidget(forms.MultiWidget):
"""
The default form widget for I18nCharField and I18nTextField. It makes
use of Django's MultiWidget mechanism and does some magic to save you
time.
"""
widget = forms.TextInput widget = forms.TextInput
def langcodes(self): def __init__(self, langcodes, field, attrs=None):
return [l[0] for l in settings.LANGUAGES]
def __init__(self, attrs=None):
widgets = [] widgets = []
for lng in self.langcodes(): self.langcodes = langcodes
self.enabled_langcodes = langcodes
self.field = field
for lng in self.langcodes:
a = copy.copy(attrs) or {} a = copy.copy(attrs) or {}
a['data-lang'] = lng a['data-lang'] = lng
widgets.append(self.widget(attrs=a)) widgets.append(self.widget(attrs=a))
@@ -56,7 +74,7 @@ class I18nWidget(forms.MultiWidget):
def decompress(self, value): def decompress(self, value):
data = [] data = []
for lng in self.langcodes(): for lng in self.langcodes:
data.append( data.append(
value.data[lng] value.data[lng]
if value is not None and isinstance(value.data, dict) and lng in value.data if value is not None and isinstance(value.data, dict) and lng in value.data
@@ -66,6 +84,29 @@ class I18nWidget(forms.MultiWidget):
data[0] = value.data data[0] = value.data
return data return data
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', None)
for i, widget in enumerate(self.widgets):
if self.langcodes[i] not in self.enabled_langcodes:
continue
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
final_attrs = dict(final_attrs, id='%s_%s' % (id_, i))
output.append(widget.render(name + '_%s' % i, widget_value, final_attrs))
return mark_safe(self.format_output(output))
def format_output(self, rendered_widgets): def format_output(self, rendered_widgets):
return '<div class="i18n-form-group">%s</div>' % super().format_output(rendered_widgets) return '<div class="i18n-form-group">%s</div>' % super().format_output(rendered_widgets)
@@ -79,16 +120,18 @@ class I18nTextarea(I18nWidget):
class I18nFormField(forms.MultiValueField): class I18nFormField(forms.MultiValueField):
"""
The form field that is used by I18nCharField and I18nTextField. It makes use
of Django's MultiValueField mechanism to create one sub-field per available
language.
"""
def compress(self, data_list): def compress(self, data_list):
langcodes = self.langcodes() langcodes = self.langcodes
data = {} data = {}
for i, value in enumerate(data_list): for i, value in enumerate(data_list):
data[langcodes[i]] = value data[langcodes[i]] = value
return LazyI18String(data) return LazyI18nString(data)
def langcodes(self):
return [l[0] for l in settings.LANGUAGES]
def clean(self, value): def clean(self, value):
found = False found = False
@@ -124,10 +167,14 @@ class I18nFormField(forms.MultiValueField):
'widget': self.widget, 'widget': self.widget,
'max_length': kwargs.pop('max_length', None), 'max_length': kwargs.pop('max_length', None),
} }
self.langcodes = kwargs.pop('langcodes', [l[0] for l in settings.LANGUAGES])
self.one_required = kwargs['required'] self.one_required = kwargs['required']
kwargs['required'] = False kwargs['required'] = False
kwargs['widget'] = kwargs['widget'](
langcodes=self.langcodes, field=self
)
defaults.update(**kwargs) defaults.update(**kwargs)
for lngcode in self.langcodes(): for lngcode in self.langcodes:
defaults['label'] = '%s (%s)' % (defaults.get('label'), lngcode) defaults['label'] = '%s (%s)' % (defaults.get('label'), lngcode)
fields.append(forms.CharField(**defaults)) fields.append(forms.CharField(**defaults))
super().__init__( super().__init__(
@@ -144,15 +191,15 @@ class I18nFieldMixin:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_python(self, value): def to_python(self, value):
if isinstance(value, LazyI18String): if isinstance(value, LazyI18nString):
return value return value
return LazyI18String(value) return LazyI18nString(value)
def get_prep_value(self, value): def get_prep_value(self, value):
if isinstance(value, LazyI18String): if isinstance(value, LazyI18nString):
value = value.data value = value.data
if isinstance(value, dict): if isinstance(value, dict):
return json.dumps(value, sort_keys=True) return json.dumps({k: v for k, v in value.items() if v}, sort_keys=True)
return value return value
def get_prep_lookup(self, lookup_type, value): def get_prep_lookup(self, lookup_type, value):
@@ -165,8 +212,16 @@ class I18nFieldMixin:
class I18nCharField(I18nFieldMixin, TextField, metaclass=SubfieldBase): class I18nCharField(I18nFieldMixin, TextField, metaclass=SubfieldBase):
"""
A CharField which takes internationalized data. Internally, a TextField dabase
field is used to store JSON. If you interact with this field, you will work
with LazyI18nString instances.
"""
widget = I18nTextInput widget = I18nTextInput
class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase): class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase):
"""
Like I18nCharField, but for TextFields.
"""
widget = I18nTextarea widget = I18nTextarea

View File

@@ -41,6 +41,10 @@ DEFAULTS = {
'default': settings.TIME_ZONE, 'default': settings.TIME_ZONE,
'type': str 'type': str
}, },
'locales': {
'default': json.dumps([settings.LANGUAGE_CODE]),
'type': list
},
'locale': { 'locale': {
'default': settings.LANGUAGE_CODE, 'default': settings.LANGUAGE_CODE,
'type': str 'type': str

View File

@@ -19,6 +19,7 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Display settings" %}</legend> <legend>{% trans "Display settings" %}</legend>
{% bootstrap_field sform.locales layout="horizontal" %}
{% bootstrap_field sform.locale layout="horizontal" %} {% bootstrap_field sform.locale layout="horizontal" %}
{% bootstrap_field sform.timezone layout="horizontal" %} {% bootstrap_field sform.timezone layout="horizontal" %}
{% bootstrap_field sform.show_date_to layout="horizontal" %} {% bootstrap_field sform.show_date_to layout="horizontal" %}

View File

@@ -0,0 +1,28 @@
from django.views.generic import edit
class EventBasedFormMixin:
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if hasattr(self.request, 'event'):
kwargs['event'] = self.request.event
return kwargs
class CreateView(EventBasedFormMixin, edit.CreateView):
"""
Like Django's default CreateView, but passes the optional event
argument to the form. This is necessary for I18nModelForms to work
properly.
"""
pass
class UpdateView(EventBasedFormMixin, edit.UpdateView):
"""
Like Django's default UpdateView, but passes the optional event
argument to the form. This is necessary for I18nModelForms to work
properly.
"""
pass

View File

@@ -2,7 +2,6 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.views.generic.edit import UpdateView
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django import forms from django import forms
@@ -15,6 +14,7 @@ from pretix.base.forms import VersionedModelForm, SettingsForm
from pretix.base.models import Event from pretix.base.models import Event
from pretix.base.signals import register_payment_providers from pretix.base.signals import register_payment_providers
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.permissions import EventPermissionRequiredMixin
from . import UpdateView
class EventUpdateForm(VersionedModelForm): class EventUpdateForm(VersionedModelForm):
@@ -71,9 +71,13 @@ class EventSettingsForm(SettingsForm):
choices=((a, a) for a in common_timezones), choices=((a, a) for a in common_timezones),
label=_("Default timezone"), label=_("Default timezone"),
) )
locales = forms.MultipleChoiceField(
choices=settings.LANGUAGES,
label=_("Available langauges"),
)
locale = forms.ChoiceField( locale = forms.ChoiceField(
choices=settings.LANGUAGES, choices=settings.LANGUAGES,
label=_("Default locale"), label=_("Default language"),
) )
user_mail_required = forms.BooleanField( user_mail_required = forms.BooleanField(
label=_("Require e-mail adresses"), label=_("Require e-mail adresses"),

View File

@@ -3,6 +3,7 @@ from itertools import product
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import transaction from django.db import transaction
from django.forms import BaseInlineFormSet
from django.forms.widgets import flatatt from django.forms.widgets import flatatt
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import format_html from django.utils.html import format_html
@@ -13,6 +14,20 @@ from pretix.base.forms import VersionedModelForm
from pretix.base.models import ItemVariation, Item from pretix.base.models import ItemVariation, Item
class I18nInlineFormSet(BaseInlineFormSet):
"""
This is equivalent to a normal BaseInlineFormset, but cares for the special needs
of I18nForms (see there for more information).
"""
def __init__(self, *args, **kwargs):
self.event = kwargs.pop('event', None)
super().__init__(*args, **kwargs)
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
class TolerantFormsetModelForm(VersionedModelForm): class TolerantFormsetModelForm(VersionedModelForm):
""" """
This is equivalent to a normal VersionedModelForm, but works around a problem that This is equivalent to a normal VersionedModelForm, but works around a problem that

View File

@@ -4,7 +4,7 @@ from django.forms import BooleanField, ModelForm
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.views.generic import ListView from django.views.generic import ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.edit import DeleteView
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.core.urlresolvers import resolve, reverse from django.core.urlresolvers import resolve, reverse
@@ -12,14 +12,15 @@ from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.shortcuts import redirect from django.shortcuts import redirect
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.forms import VersionedModelForm from pretix.base.forms import VersionedModelForm, I18nModelForm
from pretix.base.models import ( from pretix.base.models import (
Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota, Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota,
Versionable) Versionable)
from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required from pretix.control.permissions import EventPermissionRequiredMixin, event_permission_required
from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField from pretix.control.views.forms import TolerantFormsetModelForm, VariationsField, I18nInlineFormSet
from pretix.control.signals import restriction_formset from pretix.control.signals import restriction_formset
from . import UpdateView, CreateView
class ItemList(ListView): class ItemList(ListView):
@@ -215,6 +216,7 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView):
formsetclass = inlineformset_factory( formsetclass = inlineformset_factory(
Property, PropertyValue, Property, PropertyValue,
form=PropertyValueForm, form=PropertyValueForm,
formset=I18nInlineFormSet,
can_order=True, can_order=True,
extra=0, extra=0,
) )
@@ -269,6 +271,7 @@ class PropertyCreate(EventPermissionRequiredMixin, CreateView):
formsetclass = inlineformset_factory( formsetclass = inlineformset_factory(
Property, PropertyValue, Property, PropertyValue,
form=PropertyValueForm, form=PropertyValueForm,
formset=I18nInlineFormSet,
can_order=True, can_order=True,
extra=3, extra=3,
) )
@@ -440,7 +443,7 @@ class QuotaList(ListView):
).prefetch_related("items") ).prefetch_related("items")
class QuotaForm(ModelForm): class QuotaForm(I18nModelForm):
def __init__(self, **kwargs): def __init__(self, **kwargs):
items = kwargs['items'] items = kwargs['items']