Data structures and form elements for internationalized data

This commit is contained in:
Raphael Michel
2015-04-03 17:38:01 +02:00
parent f4d3679841
commit 03df35bccd
14 changed files with 345 additions and 68 deletions
+141
View File
@@ -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 '<LazyI18nString: %s>' % 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
@@ -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'),
),
]
+16 -13
View File
@@ -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)
+1 -1
View File
@@ -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":
"""
@@ -43,7 +43,7 @@
{% for line in items.positions %}
<div class="row-fluid product-row">
<div class="col-md-4 col-xs-6">
<strong>{{ line.item }}</strong>
<strong>{{ line.item.name }}</strong>
{% if line.variation %}
{{ line.variation }}
{% endif %}
+2
View File
@@ -0,0 +1,2 @@
EMAIL_PORT = 1025
EMAIL_HOST = '127.0.0.1'
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

@@ -14,7 +14,7 @@
<h4 class="panel-title">
<a data-toggle="collapse" href="#cp{{ form.pos.identity }}"
data-parent="#questions_accordion">
<strong>{{ form.pos.item }}</strong>
<strong>{{ form.pos.item.name }}</strong>
{% if form.pos.variation %}
{{ form.pos.variation }}
{% endif %}
@@ -2,7 +2,7 @@
{% for line in cart.positions %}
<div class="row-fluid cart-row">
<div class="col-md-4 col-xs-6">
<strong>{{ line.item }}</strong>
<strong>{{ line.item.name }}</strong>
{% if line.variation %}
{{ line.variation }}
{% endif %}
@@ -77,7 +77,7 @@
{% if item.short_description %}<p>{{ item.short_description }}</p>{% endif %}
</div>
<div class="col-md-2 col-xs-6 price">
{{ event.currency }} {{ item.price|floatformat:2 }}
{{ event.currency }} {{ item.price }}
{% if item.tax_rate %}
<br /><small>{% blocktrans trimmed with rate=item.tax_rate %}
incl. {{ rate }}% taxes
+1 -1
View File
@@ -112,8 +112,8 @@ LOCALE_PATHS = (
from django.utils.translation import ugettext_lazy as _ # NOQA
LANGUAGES = (
('de', _('German')),
('en', _('English')),
('de', _('German')),
)
+28 -32
View File
@@ -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'],
+16 -16
View File
@@ -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)
+1 -1
View File
@@ -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'))