diff --git a/src/pretix/base/forms.py b/src/pretix/base/forms.py index 99445fd12..196863fce 100644 --- a/src/pretix/base/forms.py +++ b/src/pretix/base/forms.py @@ -1,11 +1,25 @@ from django.forms.models import ModelFormMetaclass, BaseModelForm from django import forms from django.utils import six +from pretix.base.i18n import I18nFormField from versions.models import Versionable 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 """ @@ -18,7 +32,7 @@ class VersionedBaseModelForm(BaseModelForm): 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 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 @@ -27,6 +41,17 @@ class VersionedModelForm(six.with_metaclass(ModelFormMetaclass, VersionedBaseMod 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): """ This form is meant to be used for modifying Event- or OrganizerSettings diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py index 4d32ab711..ece74f499 100644 --- a/src/pretix/base/i18n.py +++ b/src/pretix/base/i18n.py @@ -4,10 +4,18 @@ from django.conf import settings from django.db.models import TextField, SubfieldBase from django import forms 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): + """ + Input data should be a dictionary which maps language codes to content. + """ self.data = data if isinstance(self.data, str) and self.data is not None: try: @@ -18,6 +26,11 @@ class LazyI18String: self.data = j 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: return "" if isinstance(self.data, dict): @@ -41,14 +54,19 @@ class LazyI18String: 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 - def langcodes(self): - return [l[0] for l in settings.LANGUAGES] - - def __init__(self, attrs=None): + def __init__(self, langcodes, field, attrs=None): 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['data-lang'] = lng widgets.append(self.widget(attrs=a)) @@ -56,7 +74,7 @@ class I18nWidget(forms.MultiWidget): def decompress(self, value): data = [] - for lng in self.langcodes(): + for lng in self.langcodes: data.append( value.data[lng] 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 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): return '
%s
' % super().format_output(rendered_widgets) @@ -79,16 +120,18 @@ class I18nTextarea(I18nWidget): 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): - langcodes = self.langcodes() + langcodes = self.langcodes data = {} for i, value in enumerate(data_list): data[langcodes[i]] = value - return LazyI18String(data) - - def langcodes(self): - return [l[0] for l in settings.LANGUAGES] + return LazyI18nString(data) def clean(self, value): found = False @@ -124,10 +167,14 @@ class I18nFormField(forms.MultiValueField): 'widget': self.widget, 'max_length': kwargs.pop('max_length', None), } + self.langcodes = kwargs.pop('langcodes', [l[0] for l in settings.LANGUAGES]) self.one_required = kwargs['required'] kwargs['required'] = False + kwargs['widget'] = kwargs['widget']( + langcodes=self.langcodes, field=self + ) defaults.update(**kwargs) - for lngcode in self.langcodes(): + for lngcode in self.langcodes: defaults['label'] = '%s (%s)' % (defaults.get('label'), lngcode) fields.append(forms.CharField(**defaults)) super().__init__( @@ -144,15 +191,15 @@ class I18nFieldMixin: super().__init__(*args, **kwargs) def to_python(self, value): - if isinstance(value, LazyI18String): + if isinstance(value, LazyI18nString): return value - return LazyI18String(value) + return LazyI18nString(value) def get_prep_value(self, value): - if isinstance(value, LazyI18String): + if isinstance(value, LazyI18nString): value = value.data 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 def get_prep_lookup(self, lookup_type, value): @@ -165,8 +212,16 @@ class I18nFieldMixin: 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 class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase): + """ + Like I18nCharField, but for TextFields. + """ widget = I18nTextarea diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index f4ae80e01..ab8c9d667 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -41,6 +41,10 @@ DEFAULTS = { 'default': settings.TIME_ZONE, 'type': str }, + 'locales': { + 'default': json.dumps([settings.LANGUAGE_CODE]), + 'type': list + }, 'locale': { 'default': settings.LANGUAGE_CODE, 'type': str diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html index c77f4a23c..92de3fcf7 100644 --- a/src/pretix/control/templates/pretixcontrol/event/settings.html +++ b/src/pretix/control/templates/pretixcontrol/event/settings.html @@ -19,6 +19,7 @@
{% trans "Display settings" %} + {% bootstrap_field sform.locales layout="horizontal" %} {% bootstrap_field sform.locale layout="horizontal" %} {% bootstrap_field sform.timezone layout="horizontal" %} {% bootstrap_field sform.show_date_to layout="horizontal" %} diff --git a/src/pretix/control/views/__init__.py b/src/pretix/control/views/__init__.py index e69de29bb..c6a463ed2 100644 --- a/src/pretix/control/views/__init__.py +++ b/src/pretix/control/views/__init__.py @@ -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 diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index c556e93e1..a2b3a3aab 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -2,7 +2,6 @@ from collections import OrderedDict from django.conf import settings from django.shortcuts import render, redirect 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.detail import SingleObjectMixin from django import forms @@ -15,6 +14,7 @@ from pretix.base.forms import VersionedModelForm, SettingsForm from pretix.base.models import Event from pretix.base.signals import register_payment_providers from pretix.control.permissions import EventPermissionRequiredMixin +from . import UpdateView class EventUpdateForm(VersionedModelForm): @@ -71,9 +71,13 @@ class EventSettingsForm(SettingsForm): choices=((a, a) for a in common_timezones), label=_("Default timezone"), ) + locales = forms.MultipleChoiceField( + choices=settings.LANGUAGES, + label=_("Available langauges"), + ) locale = forms.ChoiceField( choices=settings.LANGUAGES, - label=_("Default locale"), + label=_("Default language"), ) user_mail_required = forms.BooleanField( label=_("Require e-mail adresses"), diff --git a/src/pretix/control/views/forms.py b/src/pretix/control/views/forms.py index 019d4ce2c..bf8b98dd8 100644 --- a/src/pretix/control/views/forms.py +++ b/src/pretix/control/views/forms.py @@ -3,6 +3,7 @@ from itertools import product from django import forms from django.core.exceptions import ValidationError from django.db import transaction +from django.forms import BaseInlineFormSet from django.forms.widgets import flatatt from django.utils.encoding import force_text from django.utils.html import format_html @@ -13,6 +14,20 @@ from pretix.base.forms import VersionedModelForm 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): """ This is equivalent to a normal VersionedModelForm, but works around a problem that diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 4bd7a23f1..83500cb3c 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -4,7 +4,7 @@ from django.forms import BooleanField, ModelForm from django.utils.functional import cached_property 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.detail import SingleObjectMixin from django.core.urlresolvers import resolve, reverse @@ -12,14 +12,15 @@ from django.http import HttpResponseRedirect, HttpResponseForbidden from django.shortcuts import redirect from django.forms.models import inlineformset_factory 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 ( Item, ItemCategory, Property, ItemVariation, PropertyValue, Question, Quota, Versionable) 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 . import UpdateView, CreateView class ItemList(ListView): @@ -215,6 +216,7 @@ class PropertyUpdate(EventPermissionRequiredMixin, UpdateView): formsetclass = inlineformset_factory( Property, PropertyValue, form=PropertyValueForm, + formset=I18nInlineFormSet, can_order=True, extra=0, ) @@ -269,6 +271,7 @@ class PropertyCreate(EventPermissionRequiredMixin, CreateView): formsetclass = inlineformset_factory( Property, PropertyValue, form=PropertyValueForm, + formset=I18nInlineFormSet, can_order=True, extra=3, ) @@ -440,7 +443,7 @@ class QuotaList(ListView): ).prefetch_related("items") -class QuotaForm(ModelForm): +class QuotaForm(I18nModelForm): def __init__(self, **kwargs): items = kwargs['items']