diff --git a/doc/development/implementation/i18n.rst b/doc/development/implementation/i18n.rst index 7bc7ba1a8..730bb8856 100644 --- a/doc/development/implementation/i18n.rst +++ b/doc/development/implementation/i18n.rst @@ -6,116 +6,18 @@ One of pretix's major selling points is its multi-language capability. We make h way to translate *user-generated content*. In our case, we need to translate strings like product names or event descriptions, so we need event organizers to be able to fill in all fields in multiple languages. -.. note:: Implementing object-level translation in a relational database is a task that requires some difficult - trade-off. We decided for a design that is not elegant on the database level (as it violates the `1NF`_) and - makes searching in the respective database fields very hard, but allows for a simple design on the ORM level - and adds only minimal performance overhead. +For this purpose, we use ``django-i18nfield`` which started out as part of pretix and then got refactored into +its own library. It has comprehensive documentation on how to work with its `strings`_, `database fields`_ and +`forms`_. -All classes and functions introduced in this document are located in ``pretix.base.i18n`` if not stated otherwise. - -Database storage ----------------- - -pretix provides two custom model field types that allow you to work with localized strings: ``I18nCharField`` and -``I18nTextField``. Both of them are stored in the database as a ``TextField`` internally, they only differ in the -default form widget that is used by ``ModelForm``. - -As pretix does not use these fields in places that need to be searched, the negative performance impact when searching -and indexing these fields in negligible, as mentioned above. Lookups are currently not even implemented on these -fields. In the database, the strings will be stored as a JSON-encoded mapping of language codes to strings. - -Whenever you interact with those fields, you will either provide or receive an instance of the following class: - -.. autoclass:: pretix.base.i18n.LazyI18nString - :members: __init__, localize, __str__ - -Usage ------ - -The following examples are given to illustrate how you can work with ``LazyI18nString``. - -.. testsetup:: * - - from pretix.base.i18n import LazyI18nString, language - -To create a LazyI18nString, we can cast a simple string: - -.. doctest:: - - >>> naive = LazyI18nString('Naive untranslated string') - >>> naive - - -Or we can provide a dictionary with multiple translations: - -.. doctest:: - - >>> translated = LazyI18nString({'en': 'English String', 'de': 'Deutscher String'}) - -We can use ``localize`` to get the string in a specific language: - -.. doctest:: - - >>> translated.localize('de') - 'Deutscher String' - - >>> translated.localize('en') - 'English String' - -If we try a locale that does not exist for the string, we might get a it either in a similar locale or in the system's default language: - -.. doctest:: - - >>> translated.localize('de-AT') - 'Deutscher String' - - >>> translated.localize('zh') - 'English String' - - >>> naive.localize('de') - 'Naive untranslated string' - -If we cast a ``LazyI18nString`` to ``str``, ``localize`` will be called with the currently active language: - -.. doctest:: - - >>> from django.utils import translation - >>> str(translated) - 'English String' - >>> translation.activate('de') - >>> str(translated) - 'Deutscher String' - -You can also use our handy context manager to set the locale temporarily: - -.. doctest:: - - >>> translation.activate('en') - >>> with language('de'): - ... str(translated) - 'Deutscher String' - >>> str(translated) - 'English String' Forms ----- -We provide i18n-aware versions of the respective form fields and widgets: ``I18nFormField`` with the ``I18nTextInput`` -and ``I18nTextarea`` widgets. They transparently allow you to use ``LazyI18nString`` values in forms and render text -inputs for multiple languages. - -.. autoclass:: pretix.base.i18n.I18nFormField - -To easily limit the displayed languages to the languages relevant to an event, there is a custom ``ModelForm`` subclass -that deals with this for you: - -.. autoclass:: pretix.base.forms.I18nModelForm - -There are equivalents for ``BaseModelFormSet`` and ``BaseInlineFormSet``: - -.. autoclass:: pretix.base.forms.I18nFormSet - -.. autoclass:: pretix.base.forms.I18nInlineFormSet +For backwards-compatibility with older parts of pretix' code base and older plugins, ``pretix.base.forms`` still +contains a number of forms that are equivalent in name and usage to their counterparts in ``i18nfield.forms`` with +the difference that they take an ``event`` keyword argument and then set the ``locales`` argument based on +``event.settings.get('locales')``. Useful utilities ---------------- @@ -135,4 +37,6 @@ action that causes the mail to be sent. .. _translation features: https://docs.djangoproject.com/en/1.9/topics/i18n/translation/ .. _GNU gettext: https://www.gnu.org/software/gettext/ -.. _1NF: https://en.wikipedia.org/wiki/First_normal_form +.. _strings: https://django-i18nfield.readthedocs.io/en/latest/strings.html +.. _database fields: https://django-i18nfield.readthedocs.io/en/latest/quickstart.html +.. _forms: https://django-i18nfield.readthedocs.io/en/latest/forms.html diff --git a/src/pretix/base/forms/__init__.py b/src/pretix/base/forms/__init__.py index 6b38ddb10..1d54d7632 100644 --- a/src/pretix/base/forms/__init__.py +++ b/src/pretix/base/forms/__init__.py @@ -1,89 +1,55 @@ import logging +import i18nfield.forms from django import forms from django.core.files import File from django.core.files.storage import default_storage from django.core.files.uploadedfile import UploadedFile -from django.forms.models import ( - BaseInlineFormSet, BaseModelForm, BaseModelFormSet, ModelFormMetaclass, -) +from django.forms.models import ModelFormMetaclass from django.utils import six from django.utils.crypto import get_random_string from django.utils.translation import ugettext_lazy as _ -from pretix.base.i18n import I18nFormField from pretix.base.models import Event logger = logging.getLogger('pretix.plugins.ticketoutputpdf') -class BaseI18nModelForm(BaseModelForm): - """ - This is a helperclass to construct an I18nModelForm. - """ +class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm): + # compatibility shim for django-i18nfield library + def __init__(self, *args, **kwargs): event = kwargs.pop('event', None) - locales = kwargs.pop('locales', None) + if event: + kwargs['locales'] = event.settings.get('locales') super().__init__(*args, **kwargs) - if event or locales: - for k, field in self.fields.items(): - if isinstance(field, I18nFormField): - field.widget.enabled_langcodes = event.settings.get('locales') if event else locales 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`` - expecting an `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 I18nFormSet(BaseModelFormSet): - """ - This is equivalent to a normal BaseModelFormset, but cares for the special needs - of I18nForms (see there for more information). - """ +class I18nFormSet(i18nfield.forms.I18nModelFormSet): + # compatibility shim for django-i18nfield library def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event', None) + event = kwargs.pop('event', None) + if event: + kwargs['locales'] = event.settings.get('locales') super().__init__(*args, **kwargs) - def _construct_form(self, i, **kwargs): - kwargs['event'] = self.event - return super()._construct_form(i, **kwargs) - @property - def empty_form(self): - form = self.form( - auto_id=self.auto_id, - prefix=self.add_prefix('__prefix__'), - empty_permitted=True, - event=self.event - ) - self.add_fields(form, None) - return form - - -class I18nInlineFormSet(BaseInlineFormSet): - """ - This is equivalent to a normal BaseInlineFormset, but cares for the special needs - of I18nForms (see there for more information). - """ +class I18nInlineFormSet(i18nfield.forms.I18nInlineFormSet): + # compatibility shim for django-i18nfield library def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event', None) + event = kwargs.pop('event', None) + if event: + kwargs['locales'] = event.settings.get('locales') super().__init__(*args, **kwargs) - def _construct_form(self, i, **kwargs): - kwargs['event'] = self.event - return super()._construct_form(i, **kwargs) - -class SettingsForm(forms.Form): +class SettingsForm(i18nfield.forms.I18nForm): """ This form is meant to be used for modifying EventSettings or OrganizerSettings. It takes care of loading the current values of the fields and saving the field inputs to the @@ -92,6 +58,7 @@ class SettingsForm(forms.Form): :param obj: The event or organizer object which should be used for the settings storage """ + BOOL_CHOICES = ( ('False', _('disabled')), ('True', _('enabled')), @@ -100,12 +67,9 @@ class SettingsForm(forms.Form): def __init__(self, *args, **kwargs): self.obj = kwargs.pop('obj', None) self.locales = kwargs.pop('locales', None) + kwargs['locales'] = self.obj.settings.get('locales') if self.obj else self.locales kwargs['initial'] = self.obj.settings.freeze() super().__init__(*args, **kwargs) - if self.obj or self.locales: - for k, field in self.fields.items(): - if isinstance(field, I18nFormField): - field.widget.enabled_langcodes = self.obj.settings.get('locales') if self.obj else self.locales def save(self): """ diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py index 0a61caf76..c1da5b358 100644 --- a/src/pretix/base/i18n.py +++ b/src/pretix/base/i18n.py @@ -1,346 +1,16 @@ -import copy -import json from contextlib import contextmanager -from typing import Dict, List, Optional, Union -from django import forms from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.db.models import Model, QuerySet, TextField from django.utils import translation from django.utils.formats import date_format, number_format -from django.utils.safestring import mark_safe from django.utils.translation import ugettext - - -class LazyI18nString: - """ - This represents an internationalized string that is/was/will be stored in the database. - """ - - def __init__(self, data: Optional[Union[str, Dict[str, str]]]): - """ - Creates a new i18n-aware string. - - :param data: If this is a dictionary, it is expected to map language codes to translations. - If this is a string that can be parsed as JSON, it will be parsed and used as such a dictionary. - If this is anything else, it will be cast to a string and used for all languages. - """ - self.data = data - if isinstance(self.data, str) and self.data is not None: - try: - j = json.loads(self.data) - except ValueError: - pass - else: - self.data = j - - def __str__(self) -> str: - """ - Evaluate the given string with respect to the currently active locale. - - If no string is available in the currently active language, this will give you - the string in the system's default language. If this is unavailable as well, it - will give you the string in the first language available. - """ - return self.localize(translation.get_language() or settings.LANGUAGE_CODE) - - def __bool__(self): - if not self.data: - return False - if isinstance(self.data, dict): - return any(self.data.values()) - return True - - def localize(self, lng: str) -> str: - """ - Evaluate the given string with respect to the locale defined by ``lng``. - - If no string is available in the currently active language, this will give you - the string in the system's default language. If this is unavailable as well, it - will give you the string in the first language available. - - :param lng: A locale code, e.g. ``de``. If you specify a code including a country - or region like ``de-AT``, exact matches will be used preferably, but if only - a ``de`` or ``de-AT`` translation exists, this might be returned as well. - """ - if self.data is None: - return "" - - if isinstance(self.data, dict): - firstpart = lng.split('-')[0] - similar = [l for l in self.data.keys() if (l.startswith(firstpart + "-") or firstpart == l) and l != lng] - if self.data.get(lng): - return self.data[lng] - elif self.data.get(firstpart): - return self.data[firstpart] - elif similar and any([self.data.get(s) for s in similar]): - for s in similar: - if self.data.get(s): - return self.data.get(s) - elif self.data.get(settings.LANGUAGE_CODE): - return self.data[settings.LANGUAGE_CODE] - elif len(self.data): - return list(self.data.items())[0][1] - else: - return "" - else: - return str(self.data) - - def __repr__(self) -> str: # NOQA - return '' % repr(self.data) - - def __lt__(self, other) -> bool: # NOQA - return str(self) < str(other) - - def __format__(self, format_spec): - return self.__str__() - - def __eq__(self, other): - if other is None: - return False - if hasattr(other, 'data'): - return self.data == other.data - return self.data == other - - class LazyGettextProxy: - def __init__(self, lazygettext): - self.lazygettext = lazygettext - - def __getitem__(self, item): - with language(item): - return str(ugettext(self.lazygettext)) - - def __contains__(self, item): - return True - - def __str__(self): - return str(ugettext(self.lazygettext)) - - @classmethod - def from_gettext(cls, lazygettext): - l = LazyI18nString({}) - l.data = cls.LazyGettextProxy(lazygettext) - return l - - -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 __init__(self, langcodes: List[str], field: forms.Field, attrs=None): - widgets = [] - self.langcodes = langcodes - self.enabled_langcodes = langcodes - self.field = field - for lng in self.langcodes: - a = copy.copy(attrs) or {} - a['lang'] = lng - widgets.append(self.widget(attrs=a)) - super().__init__(widgets, attrs) - - def decompress(self, value): - data = [] - first_enabled = None - any_filled = False - any_enabled_filled = False - if not isinstance(value, LazyI18nString): - value = LazyI18nString(value) - for i, lng in enumerate(self.langcodes): - dataline = ( - value.data[lng] - if value is not None and ( - isinstance(value.data, dict) or isinstance(value.data, LazyI18nString.LazyGettextProxy) - ) and lng in value.data - else None - ) - any_filled = any_filled or (lng in self.enabled_langcodes and dataline) - if not first_enabled and lng in self.enabled_langcodes: - first_enabled = i - if dataline: - any_enabled_filled = True - data.append(dataline) - if value and not isinstance(value.data, dict): - data[first_enabled] = value.data - elif value and not any_enabled_filled: - data[first_enabled] = value.localize(self.enabled_langcodes[0]) - 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), - title=self.langcodes[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) - - -class I18nTextInput(I18nWidget): - widget = forms.TextInput - - -class I18nTextarea(I18nWidget): - widget = forms.Textarea - - -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. - - It contains special treatment to make sure that a field marked as "required" is validated - as "filled out correctly" if *at least one* translation is filled it. It is never required - to fill in all of them. This has the drawback that the HTML property ``required`` is set on - none of the fields as this would lead to irritating behaviour. - - :param langcodes: An iterable of locale codes that the widget should render a field for. If - omitted, fields will be rendered for all languages supported by pretix. - """ - - def compress(self, data_list): - langcodes = self.langcodes - data = {} - for i, value in enumerate(data_list): - data[langcodes[i]] = value - return LazyI18nString(data) - - def clean(self, value): - if isinstance(value, LazyI18nString): - # This happens e.g. if the field is disabled - return value - found = False - clean_data = [] - errors = [] - for i, field in enumerate(self.fields): - try: - field_value = value[i] - except IndexError: - field_value = None - if field_value not in self.empty_values: - found = True - try: - clean_data.append(field.clean(field_value)) - except forms.ValidationError as e: - # Collect all validation errors in a single list, which we'll - # raise at the end of clean(), rather than raising a single - # exception for the first error we encounter. Skip duplicates. - errors.extend(m for m in e.error_list if m not in errors) - if errors: - raise forms.ValidationError(errors) - if self.one_required and not found: - raise forms.ValidationError(self.error_messages['required'], code='required') - - out = self.compress(clean_data) - self.validate(out) - self.run_validators(out) - return out - - def __init__(self, *args, **kwargs): - fields = [] - defaults = { - '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.get('required', True) - kwargs['required'] = False - kwargs['widget'] = kwargs['widget']( - langcodes=self.langcodes, field=self, **kwargs.pop('widget_kwargs', {}) - ) - defaults.update(**kwargs) - for lngcode in self.langcodes: - defaults['label'] = '%s (%s)' % (defaults.get('label'), lngcode) - fields.append(forms.CharField(**defaults)) - super().__init__( - fields=fields, require_all_fields=False, *args, **kwargs - ) - - -class I18nFieldMixin: - form_class = I18nFormField - widget = I18nTextInput - - def __init__(self, *args, **kwargs): - self.event = kwargs.pop('event', None) - super().__init__(*args, **kwargs) - - def to_python(self, value): - if isinstance(value, LazyI18nString): - return value - return LazyI18nString(value) - - def get_prep_value(self, value): - if isinstance(value, LazyI18nString): - value = value.data - if isinstance(value, dict): - 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): # NOQA - raise TypeError('Lookups on i18n string currently not supported.') - - def from_db_value(self, value, expression, connection, context): - return LazyI18nString(value) - - def formfield(self, **kwargs): - defaults = {'form_class': self.form_class, 'widget': self.widget} - defaults.update(kwargs) - return super().formfield(**defaults) - - -class I18nCharField(I18nFieldMixin, TextField): - """ - 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): - """ - Like I18nCharField, but for TextFields. - """ - widget = I18nTextarea - - -class I18nJSONEncoder(DjangoJSONEncoder): - def default(self, obj): - if isinstance(obj, LazyI18nString): - return obj.data - elif isinstance(obj, QuerySet): - return list(obj) - elif isinstance(obj, Model): - return {'type': obj.__class__.__name__, 'id': obj.id} - else: - return super().default(obj) +from i18nfield.fields import ( # noqa + I18nCharField, I18nTextarea, I18nTextField, I18nTextInput, +) +from i18nfield.forms import I18nFormField # noqa +# Compatibility imports +from i18nfield.strings import LazyI18nString # noqa +from i18nfield.utils import I18nJSONEncoder # noqa class LazyDate: diff --git a/src/pretix/base/migrations/0001_squashed_0028_auto_20160816_1242.py b/src/pretix/base/migrations/0001_squashed_0028_auto_20160816_1242.py index a47d17b11..8faed63d4 100644 --- a/src/pretix/base/migrations/0001_squashed_0028_auto_20160816_1242.py +++ b/src/pretix/base/migrations/0001_squashed_0028_auto_20160816_1242.py @@ -8,11 +8,11 @@ from decimal import Decimal import django.core.validators import django.db.models.deletion +import i18nfield.fields from django.conf import settings from django.contrib.auth.hashers import make_password from django.db import migrations, models -import pretix.base.i18n import pretix.base.models.base import pretix.base.models.invoices import pretix.base.models.items @@ -107,7 +107,7 @@ class Migration(migrations.Migration): name='Event', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name')), + ('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')), ('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')), ('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')), ('date_from', models.DateTimeField(verbose_name='Event start time')), @@ -163,9 +163,9 @@ class Migration(migrations.Migration): name='Item', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name')), + ('name', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Item name')), ('active', models.BooleanField(default=True, verbose_name='Active')), - ('description', pretix.base.i18n.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')), + ('description', i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')), ('default_price', models.DecimalField(decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), ('tax_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Taxes included in percent')), ('admission', models.BooleanField(default=False, help_text='Whether or not buying this product allows a person to enter your event', verbose_name='Is an admission ticket')), @@ -185,10 +185,10 @@ class Migration(migrations.Migration): name='ItemCategory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Category name')), + ('name', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Category name')), ('position', models.IntegerField(default=0)), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='pretixbase.Event')), - ('description', pretix.base.i18n.I18nTextField(blank=True, verbose_name='Category description')), + ('description', i18nfield.fields.I18nTextField(blank=True, verbose_name='Category description')), ], options={ 'verbose_name': 'Product category', @@ -201,7 +201,7 @@ class Migration(migrations.Migration): name='ItemVariation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Description')), + ('value', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Description')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('position', models.PositiveIntegerField(default=0, verbose_name='Position')), ('default_price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), @@ -309,7 +309,7 @@ class Migration(migrations.Migration): name='Question', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('question', pretix.base.i18n.I18nTextField(verbose_name='Question')), + ('question', i18nfield.fields.I18nTextField(verbose_name='Question')), ('type', models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5, verbose_name='Question type')), ('required', models.BooleanField(default=False, verbose_name='Required question')), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='pretixbase.Event')), @@ -566,7 +566,7 @@ class Migration(migrations.Migration): name='QuestionOption', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('answer', pretix.base.i18n.I18nCharField(verbose_name='Answer')), + ('answer', i18nfield.fields.I18nCharField(verbose_name='Answer')), ], ), migrations.AlterField( diff --git a/src/pretix/base/migrations/0002_auto_20160209_0940.py b/src/pretix/base/migrations/0002_auto_20160209_0940.py index 06cc0df6f..43edbdb36 100644 --- a/src/pretix/base/migrations/0002_auto_20160209_0940.py +++ b/src/pretix/base/migrations/0002_auto_20160209_0940.py @@ -7,10 +7,10 @@ from decimal import Decimal import django.core.validators import django.db.models.deletion +import i18nfield.fields from django.conf import settings from django.db import migrations, models -import pretix.base.i18n import pretix.base.models.base import pretix.base.models.items import pretix.base.models.orders @@ -64,7 +64,7 @@ class Migration(migrations.Migration): name='Event', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name')), + ('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')), ('slug', models.SlugField(help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. This is being used in addresses and bank transfer references.', validators=[django.core.validators.RegexValidator(message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$')], verbose_name='Slug')), ('currency', models.CharField(default='EUR', max_length=10, verbose_name='Default currency')), ('date_from', models.DateTimeField(verbose_name='Event start time')), @@ -119,9 +119,9 @@ class Migration(migrations.Migration): name='Item', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name')), + ('name', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Item name')), ('active', models.BooleanField(default=True, verbose_name='Active')), - ('description', pretix.base.i18n.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')), + ('description', i18nfield.fields.I18nTextField(blank=True, help_text='This is shown below the product name in lists.', null=True, verbose_name='Description')), ('default_price', models.DecimalField(decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), ('tax_rate', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Taxes included in percent')), ('admission', models.BooleanField(default=False, help_text='Whether or not buying this product allows a person to enter your event', verbose_name='Is an admission ticket')), @@ -141,7 +141,7 @@ class Migration(migrations.Migration): name='ItemCategory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Category name')), + ('name', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Category name')), ('position', models.IntegerField(default=0)), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categories', to='pretixbase.Event')), ], @@ -156,7 +156,7 @@ class Migration(migrations.Migration): name='ItemVariation', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Description')), + ('value', i18nfield.fields.I18nCharField(max_length=255, verbose_name='Description')), ('active', models.BooleanField(default=True, verbose_name='Active')), ('position', models.PositiveIntegerField(default=0, verbose_name='Position')), ('default_price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True, verbose_name='Default price')), @@ -264,7 +264,7 @@ class Migration(migrations.Migration): name='Question', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('question', pretix.base.i18n.I18nTextField(verbose_name='Question')), + ('question', i18nfield.fields.I18nTextField(verbose_name='Question')), ('type', models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No')], max_length=5, verbose_name='Question type')), ('required', models.BooleanField(default=False, verbose_name='Required question')), ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='pretixbase.Event')), diff --git a/src/pretix/base/migrations/0018_auto_20160326_1104.py b/src/pretix/base/migrations/0018_auto_20160326_1104.py index f0be79957..b89199fbd 100644 --- a/src/pretix/base/migrations/0018_auto_20160326_1104.py +++ b/src/pretix/base/migrations/0018_auto_20160326_1104.py @@ -3,10 +3,9 @@ from __future__ import unicode_literals import django.db.models.deletion +import i18nfield.fields from django.db import migrations, models -import pretix.base.i18n - class Migration(migrations.Migration): @@ -19,7 +18,7 @@ class Migration(migrations.Migration): name='QuestionOption', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('answer', pretix.base.i18n.I18nCharField(verbose_name='Answer')), + ('answer', i18nfield.fields.I18nCharField(verbose_name='Answer')), ], ), migrations.AlterField( diff --git a/src/pretix/base/migrations/0020_auto_20160421_1943.py b/src/pretix/base/migrations/0020_auto_20160421_1943.py index b6e5143b8..2593f21f7 100644 --- a/src/pretix/base/migrations/0020_auto_20160421_1943.py +++ b/src/pretix/base/migrations/0020_auto_20160421_1943.py @@ -2,10 +2,9 @@ # Generated by Django 1.9.4 on 2016-04-21 19:43 from __future__ import unicode_literals +import i18nfield.fields from django.db import migrations, models -import pretix.base.i18n - class Migration(migrations.Migration): @@ -17,7 +16,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='itemcategory', name='description', - field=pretix.base.i18n.I18nTextField(blank=True, verbose_name='Category description'), + field=i18nfield.fields.I18nTextField(blank=True, verbose_name='Category description'), ), migrations.AlterField( model_name='questionanswer', diff --git a/src/pretix/base/migrations/0050_orderposition_positionid_squashed_0061_event_location.py b/src/pretix/base/migrations/0050_orderposition_positionid_squashed_0061_event_location.py index 115cd9738..a0fa7b368 100644 --- a/src/pretix/base/migrations/0050_orderposition_positionid_squashed_0061_event_location.py +++ b/src/pretix/base/migrations/0050_orderposition_positionid_squashed_0061_event_location.py @@ -6,10 +6,10 @@ import django.core.validators import django.db.migrations.operations.special import django.db.models.deletion import django.utils.timezone +import i18nfield.fields from django.conf import settings from django.db import migrations, models -import pretix.base.i18n import pretix.base.models.event import pretix.base.models.orders import pretix.base.models.organizer @@ -214,6 +214,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='event', name='location', - field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'), + field=i18nfield.fields.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'), ), ] diff --git a/src/pretix/base/migrations/0061_event_location.py b/src/pretix/base/migrations/0061_event_location.py index 70f363b6c..7f497bccc 100644 --- a/src/pretix/base/migrations/0061_event_location.py +++ b/src/pretix/base/migrations/0061_event_location.py @@ -2,10 +2,9 @@ # Generated by Django 1.10.5 on 2017-02-01 04:31 from __future__ import unicode_literals +import i18nfield.fields from django.db import migrations -import pretix.base.i18n - class Migration(migrations.Migration): @@ -17,6 +16,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='event', name='location', - field=pretix.base.i18n.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'), + field=i18nfield.fields.I18nCharField(blank=True, max_length=200, null=True, verbose_name='Location'), ), ] diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index 583d0246b..fc40dcfea 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -6,8 +6,7 @@ from django.db import models from django.db.models.signals import post_delete from django.dispatch import receiver from django.utils.crypto import get_random_string - -from pretix.base.i18n import I18nJSONEncoder +from i18nfield.utils import I18nJSONEncoder def cachedfile_name(instance, filename: str) -> str: diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index ac699f82e..32c81fef6 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -14,9 +14,9 @@ from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.timezone import make_aware, now from django.utils.translation import ugettext_lazy as _ +from i18nfield.fields import I18nCharField from pretix.base.email import CustomSMTPBackend -from pretix.base.i18n import I18nCharField from pretix.base.models.base import LoggedModel from pretix.base.settings import SettingsProxy from pretix.base.validators import EventSlugBlacklistValidator diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index ca0f3fa13..93cb9d652 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -10,9 +10,9 @@ from django.db.models import F, Func, Q, Sum from django.utils.functional import cached_property from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from i18nfield.fields import I18nCharField, I18nTextField from pretix.base.decimal import round_decimal -from pretix.base.i18n import I18nCharField, I18nTextField from pretix.base.models.base import LoggedModel from .event import Event diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 6adf435b3..fdd0af119 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -12,9 +12,10 @@ from django.http import HttpRequest from django.template.loader import get_template from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ +from i18nfield.forms import I18nFormField, I18nTextarea +from i18nfield.strings import LazyI18nString from pretix.base.decimal import round_decimal -from pretix.base.i18n import I18nFormField, I18nTextarea, LazyI18nString from pretix.base.models import Event, Order, Quota from pretix.base.settings import SettingsSandbox from pretix.base.signals import register_payment_providers diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index c8d130fe1..129dd269a 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -10,6 +10,7 @@ from django.db import transaction from django.utils.formats import date_format, localize from django.utils.timezone import now from django.utils.translation import pgettext, ugettext as _ +from i18nfield.strings import LazyI18nString from reportlab.lib import pagesizes from reportlab.lib.styles import ParagraphStyle, StyleSheet1 from reportlab.lib.units import mm @@ -21,7 +22,7 @@ from reportlab.platypus import ( Table, TableStyle, ) -from pretix.base.i18n import LazyI18nString, language +from pretix.base.i18n import language from pretix.base.models import Invoice, InvoiceAddress, InvoiceLine, Order from pretix.base.services.async import TransactionAwareTask from pretix.base.signals import register_payment_providers diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 5eed28c37..17ae7d86e 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -8,9 +8,10 @@ from django.conf import settings from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import get_template from django.utils.translation import ugettext as _ +from i18nfield.strings import LazyI18nString from inlinestyler.utils import inline_css -from pretix.base.i18n import LazyI18nString, language +from pretix.base.i18n import language from pretix.base.models import Event, InvoiceAddress, Order from pretix.celery_app import app from pretix.multidomain.urlreverse import build_absolute_uri diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 5fa933b56..0e2f8661c 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -12,7 +12,7 @@ from django.core.files.storage import default_storage from django.db.models import Model from django.utils.translation import ugettext_noop -from pretix.base.i18n import LazyI18nString +from i18nfield.strings import LazyI18nString from pretix.base.models.settings import GlobalSetting DEFAULTS = { diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 4c39d2aae..c5710cf22 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -4,10 +4,10 @@ from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.utils.timezone import get_current_timezone_name from django.utils.translation import ugettext_lazy as _ +from i18nfield.forms import I18nFormField, I18nTextarea from pytz import common_timezones from pretix.base.forms import I18nModelForm, SettingsForm -from pretix.base.i18n import I18nFormField, I18nTextarea from pretix.base.models import Event, Organizer from pretix.control.forms import ExtFileField diff --git a/src/pretix/control/forms/global_settings.py b/src/pretix/control/forms/global_settings.py index c45372740..275d2ec9a 100644 --- a/src/pretix/control/forms/global_settings.py +++ b/src/pretix/control/forms/global_settings.py @@ -1,9 +1,9 @@ from collections import OrderedDict from django.utils.translation import ugettext_lazy as _ +from i18nfield.forms import I18nFormField, I18nTextInput from pretix.base.forms import SettingsForm -from pretix.base.i18n import I18nFormField, I18nTextInput from pretix.base.settings import GlobalSettingsObject from pretix.base.signals import register_global_settings diff --git a/src/pretix/control/forms/item.py b/src/pretix/control/forms/item.py index be347ecbd..aaf0b0e7d 100644 --- a/src/pretix/control/forms/item.py +++ b/src/pretix/control/forms/item.py @@ -5,9 +5,9 @@ from django.core.exceptions import ValidationError from django.forms import BooleanField, ModelMultipleChoiceField from django.forms.formsets import DELETION_FIELD_NAME from django.utils.translation import ugettext as __, ugettext_lazy as _ +from i18nfield.forms import I18nFormField, I18nTextarea from pretix.base.forms import I18nFormSet, I18nModelForm -from pretix.base.i18n import I18nFormField, I18nTextarea from pretix.base.models import ( Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota, ) diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index 383b225c7..c05dae008 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -4,8 +4,8 @@ from decimal import Decimal from django.dispatch import receiver from django.utils import formats from django.utils.translation import ugettext_lazy as _ +from i18nfield.strings import LazyI18nString -from pretix.base.i18n import LazyI18nString from pretix.base.models import Event, ItemVariation, LogEntry from pretix.base.signals import logentry_display diff --git a/src/pretix/plugins/banktransfer/payment.py b/src/pretix/plugins/banktransfer/payment.py index 968457b94..caa2f3853 100644 --- a/src/pretix/plugins/banktransfer/payment.py +++ b/src/pretix/plugins/banktransfer/payment.py @@ -3,8 +3,9 @@ from collections import OrderedDict from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ +from i18nfield.fields import I18nFormField, I18nTextarea +from i18nfield.strings import LazyI18nString -from pretix.base.i18n import I18nFormField, I18nTextarea, LazyI18nString from pretix.base.payment import BasePaymentProvider diff --git a/src/pretix/plugins/sendmail/forms.py b/src/pretix/plugins/sendmail/forms.py index b6bdb63c1..2df6100c5 100644 --- a/src/pretix/plugins/sendmail/forms.py +++ b/src/pretix/plugins/sendmail/forms.py @@ -1,7 +1,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ +from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput -from pretix.base.i18n import I18nFormField, I18nTextarea, I18nTextInput from pretix.base.models import Order diff --git a/src/pretix/presale/context.py b/src/pretix/presale/context.py index d435474d5..2389add9f 100644 --- a/src/pretix/presale/context.py +++ b/src/pretix/presale/context.py @@ -1,7 +1,7 @@ from django.conf import settings from django.core.files.storage import default_storage +from i18nfield.strings import LazyI18nString -from pretix.base.i18n import LazyI18nString from pretix.base.settings import GlobalSettingsObject from .signals import footer_link, html_head diff --git a/src/requirements/production.txt b/src/requirements/production.txt index 9bc8a900f..2fc3dc1c5 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -28,6 +28,7 @@ django-markup markdown bleach raven +django-i18nfield # Stripe stripe==1.22.* # PayPal diff --git a/src/setup.py b/src/setup.py index 0ea75f979..4935f1cef 100644 --- a/src/setup.py +++ b/src/setup.py @@ -97,7 +97,8 @@ setup( 'redis==2.10.5', 'stripe==1.22.*', 'chardet>=2.3,<3', - 'mt-940==3.2' + 'mt-940==3.2', + 'django-i18nfield' ], extras_require={ 'dev': [ diff --git a/src/tests/base/test_i18n.py b/src/tests/base/test_i18n.py index 546427f09..dbaa6fde5 100644 --- a/src/tests/base/test_i18n.py +++ b/src/tests/base/test_i18n.py @@ -1,8 +1,8 @@ from django.test import TestCase from django.utils import translation from django.utils.timezone import now +from i18nfield.strings import LazyI18nString -from pretix.base.i18n import LazyI18nString from pretix.base.models import Event, ItemCategory, Organizer diff --git a/src/tests/base/test_settings.py b/src/tests/base/test_settings.py index 18094fc19..65f89525c 100644 --- a/src/tests/base/test_settings.py +++ b/src/tests/base/test_settings.py @@ -6,9 +6,9 @@ from django.core.files.storage import default_storage from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase from django.utils.timezone import now +from i18nfield.strings import LazyI18nString from pretix.base import settings -from pretix.base.i18n import LazyI18nString from pretix.base.models import Event, Organizer, User from pretix.base.settings import SettingsSandbox from pretix.control.forms.global_settings import GlobalSettingsObject