mirror of
https://github.com/pretix/pretix.git
synced 2026-05-09 15:54:03 +00:00
Make available languages configurable
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user