Use django-i18nfield library (#418)

This commit is contained in:
Raphael Michel
2017-02-27 21:16:28 +01:00
committed by GitHub
parent 81adbb3813
commit 8b7d2314b8
27 changed files with 84 additions and 544 deletions

View File

@@ -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
<LazyI18nString: 'Naive untranslated string'>
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

View File

@@ -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):
"""

View File

@@ -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 '<LazyI18nString: %s>' % 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 '<div class="i18n-form-group">%s</div>' % 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:

View File

@@ -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(

View File

@@ -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')),

View File

@@ -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(

View File

@@ -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',

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -28,6 +28,7 @@ django-markup
markdown
bleach
raven
django-i18nfield
# Stripe
stripe==1.22.*
# PayPal

View File

@@ -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': [

View File

@@ -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

View File

@@ -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