diff --git a/src/pretix/base/i18n.py b/src/pretix/base/i18n.py new file mode 100644 index 0000000000..08ff6c39d5 --- /dev/null +++ b/src/pretix/base/i18n.py @@ -0,0 +1,141 @@ +import copy +import json +from django.conf import settings +from django.db.models import TextField, SubfieldBase +from django import forms +from django.utils import translation + + +class LazyI18String: + def __init__(self, data): + 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): + if self.data is None: + return "" + if isinstance(self.data, dict): + lng = translation.get_language() + if lng in self.data and self.data[lng]: + return self.data[lng] + elif settings.LANGUAGE_CODE in self.data and self.data[settings.LANGUAGE_CODE]: + return self.data[settings.LANGUAGE_CODE] + elif len(self.data): + return self.data.items()[0][1] + else: + return "" + else: + return str(self.data) + + def __repr__(self): + return '' % repr(self.data) + + def __lt__(self, other): + return str(self) < str(other) + + +class I18nWidget(forms.MultiWidget): + widget = forms.TextInput + + def langcodes(self): + return [l[0] for l in settings.LANGUAGES] + + def __init__(self, attrs=None): + widgets = [] + for lng in self.langcodes(): + a = copy.copy(attrs) or {} + a['data-lang'] = lng + widgets.append(self.widget(attrs=a)) + super().__init__(widgets, attrs) + + def decompress(self, value): + data = [] + for lng in self.langcodes(): + data.append( + value.data[lng] + if value is not None and value.data is not None and lng in value.data + else None + ) + return data + + +class I18nTextInput(I18nWidget): + widget = forms.TextInput + + +class I18nTextarea(I18nWidget): + widget = forms.Textarea + + +class I18nFormField(forms.MultiValueField): + + def compress(self, data_list): + 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] + + def __init__(self, *args, **kwargs): + fields = [] + defaults = { + 'widget': self.widget, + 'max_length': kwargs.pop('max_length', None), + } + kwargs['required'] = False + 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: + # TODO: Formfield + # TODO: Correct null/blank validating + 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, LazyI18String): + return value + return LazyI18String(value) + + def get_prep_value(self, value): + if isinstance(value, LazyI18String): + value = value.data + if isinstance(value, dict): + return json.dumps(value, sort_keys=True) + return value + + def get_prep_lookup(self, lookup_type, value): + raise TypeError('Lookups on i18n string currently not supported.') + + def formfield(self, **kwargs): + defaults = {'form_class': self.form_class, 'widget': self.widget} + defaults.update(kwargs) + return super().formfield(**defaults) + + +class I18nCharField(I18nFieldMixin, TextField, metaclass=SubfieldBase): + # TODO: Check max length + widget = I18nTextInput + + +class I18nTextField(I18nFieldMixin, TextField, metaclass=SubfieldBase): + widget = I18nTextarea diff --git a/src/pretix/base/migrations/0024_auto_20150401_1239.py b/src/pretix/base/migrations/0024_auto_20150401_1239.py new file mode 100644 index 0000000000..d79c6e5d6e --- /dev/null +++ b/src/pretix/base/migrations/0024_auto_20150401_1239.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import pretix.base.i18n + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0023_auto_20150401_0954'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='name', + field=pretix.base.i18n.I18nCharField(max_length=200, verbose_name='Name'), + ), + migrations.AlterField( + model_name='item', + name='long_description', + field=pretix.base.i18n.I18nTextField(null=True, blank=True, verbose_name='Long description'), + ), + migrations.AlterField( + model_name='item', + name='name', + field=pretix.base.i18n.I18nCharField(max_length=255, verbose_name='Item name'), + ), + migrations.AlterField( + model_name='item', + name='short_description', + field=pretix.base.i18n.I18nTextField(null=True, blank=True, verbose_name='Short description', help_text='This is shown below the product name in lists.'), + ), + migrations.AlterField( + model_name='property', + name='name', + field=pretix.base.i18n.I18nCharField(max_length=250, verbose_name='Property name'), + ), + migrations.AlterField( + model_name='propertyvalue', + name='value', + field=pretix.base.i18n.I18nCharField(max_length=250, verbose_name='Value'), + ), + migrations.AlterField( + model_name='question', + name='question', + field=pretix.base.i18n.I18nTextField(verbose_name='Question'), + ), + ] diff --git a/src/pretix/base/models.py b/src/pretix/base/models.py index 5b89cee121..fc9d0a8fd4 100644 --- a/src/pretix/base/models.py +++ b/src/pretix/base/models.py @@ -15,6 +15,7 @@ from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from django.template.defaultfilters import date as _date from django.core.validators import RegexValidator +from pretix.base.i18n import I18nCharField, I18nTextField from pretix.base.settings import SettingsProxy import six from versions.models import Versionable as BaseVersionable @@ -379,8 +380,10 @@ class Event(Versionable): organizer = VersionedForeignKey(Organizer, related_name="events", on_delete=models.PROTECT) - name = models.CharField(max_length=200, - verbose_name=_("Name")) + name = I18nCharField( + max_length=200, + verbose_name=_("Name"), + ) slug = models.SlugField( max_length=50, db_index=True, help_text=_( @@ -424,7 +427,7 @@ class Event(Versionable): ordering = ("date_from", "name") def __str__(self): - return self.name + return str(self.name) def save(self, *args, **kwargs): obj = super().save(*args, **kwargs) @@ -594,7 +597,7 @@ class Property(Versionable): Event, related_name="properties", ) - name = models.CharField( + name = I18nCharField( max_length=250, verbose_name=_("Property name"), ) @@ -604,7 +607,7 @@ class Property(Versionable): verbose_name_plural = _("Product properties") def __str__(self): - return self.name + return str(self.name) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) @@ -635,7 +638,7 @@ class PropertyValue(Versionable): on_delete=models.CASCADE, related_name="values" ) - value = models.CharField( + value = I18nCharField( max_length=250, verbose_name=_("Value"), ) @@ -704,7 +707,7 @@ class Question(Versionable): Event, related_name="questions", ) - question = models.TextField( + question = I18nTextField( verbose_name=_("Question"), ) type = models.CharField( @@ -722,7 +725,7 @@ class Question(Versionable): verbose_name_plural = _("Questions") def __str__(self): - return self.question + return str(self.question) def delete(self, *args, **kwargs): super().delete(*args, **kwargs) @@ -777,20 +780,20 @@ class Item(Versionable): blank=True, null=True, verbose_name=_("Category"), ) - name = models.CharField( + name = I18nCharField( max_length=255, - verbose_name=_("Item name") + verbose_name=_("Item name"), ) active = models.BooleanField( default=True, verbose_name=_("Active"), ) - short_description = models.TextField( + short_description = I18nTextField( verbose_name=_("Short description"), help_text=_("This is shown below the product name in lists."), null=True, blank=True, ) - long_description = models.TextField( + long_description = I18nTextField( verbose_name=_("Long description"), null=True, blank=True, ) @@ -839,7 +842,7 @@ class Item(Versionable): verbose_name_plural = _("Products") def __str__(self): - return self.name + return str(self.name) def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/src/pretix/base/types.py b/src/pretix/base/types.py index 605ac69a40..2492efe3f4 100644 --- a/src/pretix/base/types.py +++ b/src/pretix/base/types.py @@ -80,7 +80,7 @@ class VariationDict(dict): ] def __str__(self): - return " – ".join([v.value for v in self.ordered_values()]) + return " – ".join([str(v.value) for v in self.ordered_values()]) def copy(self) -> "VariationDict": """ diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index c890ddae12..e3e4417b23 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -43,7 +43,7 @@ {% for line in items.positions %}
- {{ line.item }} + {{ line.item.name }} {% if line.variation %} – {{ line.variation }} {% endif %} diff --git a/src/pretix/local_settings.py b/src/pretix/local_settings.py new file mode 100644 index 0000000000..bb21042958 --- /dev/null +++ b/src/pretix/local_settings.py @@ -0,0 +1,2 @@ +EMAIL_PORT = 1025 +EMAIL_HOST = '127.0.0.1' diff --git a/src/pretix/presale/static/pretixpresale/pdf/ticket_default_a4.svg b/src/pretix/presale/static/pretixpresale/pdf/ticket_default_a4.svg new file mode 100644 index 0000000000..d5dc1fe66f --- /dev/null +++ b/src/pretix/presale/static/pretixpresale/pdf/ticket_default_a4.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + Your Ticket + + + diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html index b9485f7a2e..8f994ecc6e 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_questions.html @@ -14,7 +14,7 @@

- {{ form.pos.item }} + {{ form.pos.item.name }} {% if form.pos.variation %} – {{ form.pos.variation }} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html index 55d883cf9e..6333c16335 100644 --- a/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_cart.html @@ -2,7 +2,7 @@ {% for line in cart.positions %}
- {{ line.item }} + {{ line.item.name }} {% if line.variation %} – {{ line.variation }} {% endif %} diff --git a/src/pretix/presale/templates/pretixpresale/event/index.html b/src/pretix/presale/templates/pretixpresale/event/index.html index 5d5c2a2cf7..a19c8e4802 100644 --- a/src/pretix/presale/templates/pretixpresale/event/index.html +++ b/src/pretix/presale/templates/pretixpresale/event/index.html @@ -77,7 +77,7 @@ {% if item.short_description %}

{{ item.short_description }}

{% endif %}
- {{ event.currency }} {{ item.price|floatformat:2 }} + {{ event.currency }} {{ item.price }} {% if item.tax_rate %}
{% blocktrans trimmed with rate=item.tax_rate %} incl. {{ rate }}% taxes diff --git a/src/pretix/settings.py b/src/pretix/settings.py index b6aae47261..9369a7a668 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -112,8 +112,8 @@ LOCALE_PATHS = ( from django.utils.translation import ugettext_lazy as _ # NOQA LANGUAGES = ( - ('de', _('German')), ('en', _('English')), + ('de', _('German')), ) diff --git a/src/tests/base/test_models.py b/src/tests/base/test_models.py index a180addf06..e1445ac130 100644 --- a/src/tests/base/test_models.py +++ b/src/tests/base/test_models.py @@ -21,21 +21,19 @@ class ItemVariationsTest(TestCase): organizer=o, name='Dummy', slug='dummy', date_from=now(), ) - p = Property.objects.create(event=cls.event, name='Size') - PropertyValue.objects.create(prop=p, value='S') - PropertyValue.objects.create(prop=p, value='M') - PropertyValue.objects.create(prop=p, value='L') - p = Property.objects.create(event=cls.event, name='Color') - PropertyValue.objects.create(prop=p, value='black') - PropertyValue.objects.create(prop=p, value='blue') + cls.p_size = Property.objects.create(event=cls.event, name='Size') + cls.pv_size_s = PropertyValue.objects.create(prop=cls.p_size, value='S') + cls.pv_size_m = PropertyValue.objects.create(prop=cls.p_size, value='M') + PropertyValue.objects.create(prop=cls.p_size, value='L') + cls.p_color = Property.objects.create(event=cls.event, name='Color') + cls.pv_color_black = PropertyValue.objects.create(prop=cls.p_color, value='black') + PropertyValue.objects.create(prop=cls.p_color, value='blue') def test_variationdict(self): i = Item.objects.create(event=self.event, name='Dummy') - p = Property.objects.get(event=self.event, name='Size') - i.properties.add(p) + i.properties.add(self.p_size) iv = ItemVariation.objects.create(item=i) - pv = PropertyValue.objects.get(prop=p, value='S') - iv.values.add(pv) + iv.values.add(self.pv_size_s) variations = i.get_all_variations() @@ -46,16 +44,16 @@ class ItemVariationsTest(TestCase): for v in vd.relevant_values(): self.assertIs(type(v), PropertyValue) - if vd[p.pk] == pv: + if vd[self.p_size.pk] == self.pv_size_s: vd1 = vd vd2 = VariationDict() - vd2[p.pk] = pv + vd2[self.p_size.pk] = self.pv_size_s self.assertEqual(vd2.identify(), vd1.identify()) self.assertEqual(vd2, vd1) - vd2[p.pk] = PropertyValue.objects.get(prop=p, value='M') + vd2[self.p_size.pk] = self.pv_size_m self.assertNotEqual(vd2.identify(), vd.identify()) self.assertNotEqual(vd2, vd1) @@ -63,7 +61,7 @@ class ItemVariationsTest(TestCase): vd3 = vd2.copy() self.assertEqual(vd3, vd2) - vd2[p.pk] = pv + vd2[self.p_size.pk] = self.pv_size_s self.assertNotEqual(vd3, vd2) vd4 = VariationDict() @@ -80,22 +78,21 @@ class ItemVariationsTest(TestCase): self.assertEqual(v[0], {}) # One property, no variations - p = Property.objects.get(event=self.event, name='Size') - i.properties.add(p) + i.properties.add(self.p_size) v = i.get_all_variations() self.assertIs(type(v), list) self.assertEqual(len(v), 3) values = [] for var in v: self.assertIs(type(var), VariationDict) - self.assertIn(p.pk, var) - self.assertIs(type(var[p.pk]), PropertyValue) - values.append(var[p.pk].value) - self.assertEqual(sorted(values), sorted(['S', 'M', 'L'])) + self.assertIn(self.p_size.pk, var) + self.assertIs(type(var[self.p_size.pk]), PropertyValue) + values.append(var[self.p_size.pk].value) + self.assertEqual(sorted([str(V) for V in values]), sorted(['S', 'M', 'L'])) # One property, one variation iv = ItemVariation.objects.create(item=i) - iv.values.add(PropertyValue.objects.get(prop=p, value='S')) + iv.values.add(self.pv_size_s) v = i.get_all_variations() self.assertIs(type(v), list) self.assertEqual(len(v), 3) @@ -107,16 +104,15 @@ class ItemVariationsTest(TestCase): self.assertEqual(iv.pk, var['variation'].pk) values.append(var['variation'].values.all()[0].value) num_variations += 1 - elif p.pk in var: - self.assertIs(type(var[p.pk]), PropertyValue) - values.append(var[p.pk].value) - self.assertEqual(sorted(values), sorted(['S', 'M', 'L'])) + elif self.p_size.pk in var: + self.assertIs(type(var[self.p_size.pk]), PropertyValue) + values.append(var[self.p_size.pk].value) + self.assertEqual(sorted([str(V) for V in values]), sorted(['S', 'M', 'L'])) self.assertEqual(num_variations, 1) # Two properties, one variation - p2 = Property.objects.get(event=self.event, name='Color') - i.properties.add(p2) - iv.values.add(PropertyValue.objects.get(prop=p2, value='black')) + i.properties.add(self.p_color) + iv.values.add(self.pv_color_black) v = i.get_all_variations() self.assertIs(type(v), list) self.assertEqual(len(v), 6) @@ -126,11 +122,11 @@ class ItemVariationsTest(TestCase): self.assertIs(type(var), VariationDict) if 'variation' in var: self.assertEqual(iv.pk, var['variation'].pk) - values.append(sorted([ivv.value for ivv in iv.values.all()])) - self.assertEqual(sorted([ivv.value for ivv in iv.values.all()]), sorted(['S', 'black'])) + values.append(sorted([str(ivv.value) for ivv in iv.values.all()])) + self.assertEqual(sorted([str(ivv.value) for ivv in iv.values.all()]), sorted(['S', 'black'])) num_variations += 1 else: - values.append(sorted([pv.value for pv in var.values()])) + values.append(sorted([str(pv.value) for pv in var.values()])) self.assertEqual(sorted(values), sorted([ ['S', 'black'], ['S', 'blue'], diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py index 422d158431..e1917b9d0a 100644 --- a/src/tests/control/test_items.py +++ b/src/tests/control/test_items.py @@ -104,15 +104,15 @@ class PropertiesTest(ItemFormTest): self.driver.get('%s/control/event/%s/%s/properties/add' % ( self.live_server_url, self.orga1.slug, self.event1.slug )) - self.driver.find_element_by_css_selector("#id_name").send_keys('Size') - self.driver.find_element_by_name("values-0-value").send_keys('S') - self.driver.find_element_by_name("values-1-value").send_keys('M') + self.driver.find_element_by_css_selector("#id_name_0").send_keys('Size') + self.driver.find_element_by_name("values-0-value_0").send_keys('S') + self.driver.find_element_by_name("values-1-value_0").send_keys('M') self.scroll_and_click(self.driver.find_element_by_class_name("btn-save")) self.driver.find_element_by_class_name("alert-success") self.assertIn("Size", self.driver.find_element_by_css_selector("#page-wrapper table").text) self.driver.find_element_by_partial_link_text("Size").click() - self.assertEqual("S", self.driver.find_element_by_name("values-0-value").get_attribute("value")) - self.assertEqual("M", self.driver.find_element_by_name("values-1-value").get_attribute("value")) + self.assertEqual("S", self.driver.find_element_by_name("values-0-value_0").get_attribute("value")) + self.assertEqual("M", self.driver.find_element_by_name("values-1-value_0").get_attribute("value")) @unittest.skipIf('TRAVIS' in os.environ, 'See CategoriesTest.test_sort for details.') def test_update(self): @@ -122,18 +122,18 @@ class PropertiesTest(ItemFormTest): self.driver.get('%s/control/event/%s/%s/properties/%s/' % ( self.live_server_url, self.orga1.slug, self.event1.slug, c.identity )) - self.driver.find_element_by_css_selector("#id_name").clear() - self.driver.find_element_by_css_selector("#id_name").send_keys('Color') + self.driver.find_element_by_css_selector("#id_name_0").clear() + self.driver.find_element_by_css_selector("#id_name_0").send_keys('Color') self.driver.find_elements_by_css_selector("div.form-group button.btn-danger")[0].click() - self.scroll_into_view(self.driver.find_element_by_name("values-1-value")) - self.driver.find_element_by_name("values-1-value").clear() - self.driver.find_element_by_name("values-1-value").send_keys('red') + self.scroll_into_view(self.driver.find_element_by_name("values-1-value_0")) + self.driver.find_element_by_name("values-1-value_0").clear() + self.driver.find_element_by_name("values-1-value_0").send_keys('red') self.driver.find_element_by_css_selector("button[data-formset-add]").click() - self.driver.find_element_by_name("values-2-value").send_keys('blue') + self.driver.find_element_by_name("values-2-value_0").send_keys('blue') self.driver.find_element_by_class_name("btn-save").click() self.driver.find_element_by_class_name("alert-success") - self.assertEqual("red", self.driver.find_element_by_name("values-0-value").get_attribute("value")) - self.assertEqual("blue", self.driver.find_element_by_name("values-1-value").get_attribute("value")) + self.assertEqual("red", self.driver.find_element_by_name("values-0-value_0").get_attribute("value")) + self.assertEqual("blue", self.driver.find_element_by_name("values-1-value_0").get_attribute("value")) def test_delete(self): c = Property.objects.create(event=self.event1, name="Size") @@ -151,7 +151,7 @@ class QuestionsTest(ItemFormTest): self.driver.get('%s/control/event/%s/%s/questions/add' % ( self.live_server_url, self.orga1.slug, self.event1.slug )) - self.driver.find_element_by_name("question").send_keys('What is your shoe size?') + self.driver.find_element_by_name("question_0").send_keys('What is your shoe size?') Select(self.driver.find_element_by_name("type")).select_by_value('N') self.driver.find_element_by_class_name("btn-save").click() self.driver.find_element_by_class_name("alert-success") @@ -162,8 +162,8 @@ class QuestionsTest(ItemFormTest): self.driver.get('%s/control/event/%s/%s/questions/%s/' % ( self.live_server_url, self.orga1.slug, self.event1.slug, c.identity )) - self.driver.find_element_by_name("question").clear() - self.driver.find_element_by_name("question").send_keys('How old are you?') + self.driver.find_element_by_name("question_0").clear() + self.driver.find_element_by_name("question_0").send_keys('How old are you?') self.scroll_and_click(self.driver.find_element_by_class_name("btn-save")) self.driver.find_element_by_class_name("alert-success") self.assertIn("How old", self.driver.find_element_by_css_selector("#page-wrapper table").text) diff --git a/src/tests/presale/test_event.py b/src/tests/presale/test_event.py index f6bc188b1d..952f89a5e2 100644 --- a/src/tests/presale/test_event.py +++ b/src/tests/presale/test_event.py @@ -17,7 +17,7 @@ class EventMiddlewareTest(BrowserTest): def test_event_header(self): self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug)) - self.assertIn(self.event.name, self.driver.find_element_by_css_selector("h1").text) + self.assertIn(str(self.event.name), self.driver.find_element_by_css_selector("h1").text) def test_not_found(self): resp = self.client.get('%s/%s/%s/' % (self.live_server_url, 'foo', 'bar'))