Compare commits

...

7 Commits

Author SHA1 Message Date
Raphael Michel
d64917a65e Bump version 2017-05-18 10:21:12 +02:00
Raphael Michel
dde95b0c5c Add release config for GitLab CI 2017-05-18 10:21:05 +02:00
Raphael Michel
596beb99be Fix critical bug in item creation 2017-05-18 10:21:05 +02:00
Raphael Michel
0581d82306 Prepare release 2017-03-28 10:37:37 +02:00
Raphael Michel
44ba1904f6 Backport minor fix 2017-03-28 10:37:02 +02:00
jlwt90
a5c7c9dd1e Fix #428 -- Timezone handling on event creation/update (#432)
* add event timezone during event creation

* add timezone handling in EventUpdate

* added event creation test cases & form cleaning bug fix
2017-03-28 10:27:11 +02:00
Raphael Michel
6d7ada4836 Sendmail plugin: Fix usage of old argument 2017-03-28 10:26:31 +02:00
8 changed files with 263 additions and 19 deletions

View File

@@ -2,11 +2,32 @@ before_script:
tests: tests:
stage: test stage: test
script: script:
- virtualenv-3.4 env - virtualenv env
- source env/bin/activate - source env/bin/activate
- pip install -U pip wheel setuptools - pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache bash .travis.sh style - XDG_CACHE_HOME=/cache bash .travis.sh style
- XDG_CACHE_HOME=/cache bash .travis.sh tests - XDG_CACHE_HOME=/cache bash .travis.sh tests
- XDG_CACHE_HOME=/cache bash .travis.sh doctests
tags: tags:
- python3 - python3
pypi:
stage: release
script:
- cp /keys/.pypirc ~/.pypirc
- virtualenv env
- source env/bin/activate
- pip install -U pip wheel setuptools
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
- cd src
- python setup.py sdist upload
- python setup.py bdist_wheel upload
tags:
- python3
only:
- release
artifacts:
paths:
- src/dist/
stages:
- test
- build
- release

View File

@@ -1 +1 @@
__version__ = "1.1.1" __version__ = "1.1.3"

View File

@@ -5,7 +5,7 @@ from django.core.validators import RegexValidator
from django.utils.timezone import get_current_timezone_name from django.utils.timezone import get_current_timezone_name
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from i18nfield.forms import I18nFormField, I18nTextarea from i18nfield.forms import I18nFormField, I18nTextarea
from pytz import common_timezones from pytz import common_timezones, timezone
from pretix.base.forms import I18nModelForm, SettingsForm from pretix.base.forms import I18nModelForm, SettingsForm
from pretix.base.models import Event, Organizer from pretix.base.models import Event, Organizer
@@ -76,12 +76,27 @@ class EventWizardBasicsForm(I18nModelForm):
def clean(self): def clean(self):
data = super().clean() data = super().clean()
if data['locale'] not in self.locales: if data.get('locale') not in self.locales:
raise ValidationError({ raise ValidationError({
'locale': _('Your default locale must also be enabled for your event (see box above).') 'locale': _('Your default locale must also be enabled for your event (see box above).')
}) })
if data.get('timezone') not in common_timezones:
raise ValidationError({
'timezone': _('Your default locale must be specified.')
})
# change timezone
zone = timezone(data.get('timezone'))
data['date_from'] = self.reset_timezone(zone, data.get('date_from'))
data['date_to'] = self.reset_timezone(zone, data.get('date_to'))
data['presale_start'] = self.reset_timezone(zone, data.get('presale_start'))
data['presale_end'] = self.reset_timezone(zone, data.get('presale_end'))
return data return data
@staticmethod
def reset_timezone(tz, dt):
return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
def clean_slug(self): def clean_slug(self):
slug = self.cleaned_data['slug'] slug = self.cleaned_data['slug']
if Event.objects.filter(slug=slug, organizer=self.organizer).exists(): if Event.objects.filter(slug=slug, organizer=self.organizer).exists():

View File

@@ -117,7 +117,18 @@ class ItemCreateForm(I18nModelForm):
) )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.cleaned_data.get('copy_from'):
self.instance.category = self.cleaned_data['copy_from'].category
self.instance.description = self.cleaned_data['copy_from'].description
self.instance.active = self.cleaned_data['copy_from'].active
self.instance.available_from = self.cleaned_data['copy_from'].available_from
self.instance.available_until = self.cleaned_data['copy_from'].available_until
self.instance.require_voucher = self.cleaned_data['copy_from'].require_voucher
self.instance.hide_without_voucher = self.cleaned_data['copy_from'].hide_without_voucher
self.instance.allow_cancel = self.cleaned_data['copy_from'].allow_cancel
instance = super().save(*args, **kwargs) instance = super().save(*args, **kwargs)
if self.cleaned_data.get('has_variations'): if self.cleaned_data.get('has_variations'):
if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations: if self.cleaned_data.get('copy_from') and self.cleaned_data.get('copy_from').has_variations:
for variation in self.cleaned_data['copy_from'].variations.all(): for variation in self.cleaned_data['copy_from'].variations.all():
@@ -128,8 +139,9 @@ class ItemCreateForm(I18nModelForm):
item=instance, value=__('Standard') item=instance, value=__('Standard')
) )
for question in Question.objects.filter(items=self.cleaned_data.get('copy_from')): if self.cleaned_data.get('copy_from'):
question.items.add(instance) for question in self.cleaned_data['copy_from'].questions.all():
question.items.add(instance)
return instance return instance

View File

@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, ListView from django.views.generic import FormView, ListView
from django.views.generic.base import TemplateView, View from django.views.generic.base import TemplateView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from pytz import timezone
from pretix.base.forms import I18nModelForm from pretix.base.forms import I18nModelForm
from pretix.base.models import ( from pretix.base.models import (
@@ -88,10 +89,21 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.get_form() form = self.get_form()
if form.is_valid() and self.sform.is_valid(): if form.is_valid() and self.sform.is_valid():
# reset timezone
zone = timezone(self.sform.cleaned_data['timezone'])
event = form.instance
event.date_from = self.reset_timezone(zone, event.date_from)
event.date_to = self.reset_timezone(zone, event.date_to)
event.presale_start = self.reset_timezone(zone, event.presale_start)
event.presale_end = self.reset_timezone(zone, event.presale_end)
return self.form_valid(form) return self.form_valid(form)
else: else:
return self.form_invalid(form) return self.form_invalid(form)
@staticmethod
def reset_timezone(tz, dt):
return tz.localize(dt.replace(tzinfo=None)) if dt is not None else None
class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event model = Event

View File

@@ -766,15 +766,6 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.')) messages.success(self.request, _('Your changes have been saved.'))
if form.cleaned_data['copy_from']:
form.instance.category = form.cleaned_data['copy_from'].category
form.instance.description = form.cleaned_data['copy_from'].description
form.instance.active = form.cleaned_data['copy_from'].active
form.instance.available_from = form.cleaned_data['copy_from'].available_from
form.instance.available_until = form.cleaned_data['copy_from'].available_until
form.instance.require_voucher = form.cleaned_data['copy_from'].require_voucher
form.instance.hide_without_voucher = form.cleaned_data['copy_from'].hide_without_voucher
form.instance.allow_cancel = form.cleaned_data['copy_from'].allow_cancel
ret = super().form_valid(form) ret = super().form_valid(form)
form.instance.log_action('pretix.event.item.added', user=self.request.user, data={ form.instance.log_action('pretix.event.item.added', user=self.request.user, data={

View File

@@ -15,11 +15,11 @@ class MailForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['subject'] = I18nFormField( self.fields['subject'] = I18nFormField(
widget=I18nTextInput, required=True, widget=I18nTextInput, required=True,
langcodes=event.settings.get('locales') locales=event.settings.get('locales')
) )
self.fields['message'] = I18nFormField( self.fields['message'] = I18nFormField(
widget=I18nTextarea, required=True, widget=I18nTextarea, required=True,
langcodes=event.settings.get('locales'), locales=event.settings.get('locales'),
help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, " help_text=_("Available placeholders: {due_date}, {event}, {order}, {order_date}, {order_url}, "
"{invoice_name}, {invoice_company}") "{invoice_name}, {invoice_company}")
) )

View File

@@ -1,6 +1,9 @@
import datetime import datetime
from decimal import Decimal from decimal import Decimal
import pytz
from i18nfield.strings import LazyI18nString
from pytz import timezone
from tests.base import SoupTest, extract_form_fields from tests.base import SoupTest, extract_form_fields
from pretix.base.models import ( from pretix.base.models import (
@@ -44,7 +47,6 @@ class EventsTest(SoupTest):
doc = self.get_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug)) doc = self.get_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug))
doc.select("[name=date_to]")[0]['value'] = "2013-12-30 17:00:00" doc.select("[name=date_to]")[0]['value'] = "2013-12-30 17:00:00"
doc.select("[name=settings-max_items_per_order]")[0]['value'] = "12" doc.select("[name=settings-max_items_per_order]")[0]['value'] = "12"
print(extract_form_fields(doc.select('.container-fluid form')[0]))
doc = self.post_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug), doc = self.post_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug),
extract_form_fields(doc.select('.container-fluid form')[0])) extract_form_fields(doc.select('.container-fluid form')[0]))
@@ -52,6 +54,27 @@ class EventsTest(SoupTest):
assert doc.select("[name=date_to]")[0]['value'] == "2013-12-30 17:00:00" assert doc.select("[name=date_to]")[0]['value'] == "2013-12-30 17:00:00"
assert doc.select("[name=settings-max_items_per_order]")[0]['value'] == "12" assert doc.select("[name=settings-max_items_per_order]")[0]['value'] == "12"
def test_settings_timezone(self):
doc = self.get_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug))
doc.select("[name=date_to]")[0]['value'] = "2013-12-30 17:00:00"
doc.select("[name=settings-max_items_per_order]")[0]['value'] = "12"
doc.select("[name=settings-timezone]")[0]['value'] = "Asia/Tokyo"
doc.find('option', {"value": "Asia/Tokyo"})['selected'] = 'selected'
doc.find('option', {"value": "UTC"}).attrs.pop('selected')
doc = self.post_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug),
extract_form_fields(doc.select('.container-fluid form')[0]))
assert len(doc.select(".alert-success")) > 0
# date_to should not be changed even though the timezone is changed
assert doc.select("[name=date_to]")[0]['value'] == "2013-12-30 17:00:00"
assert doc.find('option', {"value": "Asia/Tokyo"})['selected'] == "selected"
assert doc.select("[name=settings-max_items_per_order]")[0]['value'] == "12"
self.event1.refresh_from_db()
# Asia/Tokyo -> GMT+9
assert self.event1.date_to.strftime('%Y-%m-%d %H:%M:%S') == "2013-12-30 08:00:00"
assert self.event1.settings.timezone == 'Asia/Tokyo'
def test_plugins(self): def test_plugins(self):
doc = self.get_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug)) doc = self.get_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug))
self.assertIn("PayPal", doc.select(".form-plugins")[0].text) self.assertIn("PayPal", doc.select(".form-plugins")[0].text)
@@ -193,3 +216,173 @@ class EventsTest(SoupTest):
data, follow=True) data, follow=True)
self.event1.settings._flush() self.event1.settings._flush()
assert self.event1.settings.get('ticket_download', as_type=bool) assert self.event1.settings.get('ticket_download', as_type=bool)
def test_create_event_unauthorized(self):
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga2.pk,
'foundation-locales': ('en', 'de')
})
assert doc.select(".alert-danger")
def test_create_invalid_default_language(self):
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga1.pk,
'foundation-locales': ('de',)
})
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'basics',
'basics-name_0': '33C3',
'basics-name_1': '33C3',
'basics-slug': '33c3',
'basics-date_from': '2016-12-27 10:00:00',
'basics-date_to': '2016-12-30 19:00:00',
'basics-location_0': 'Hamburg',
'basics-location_1': 'Hamburg',
'basics-currency': 'EUR',
'basics-locale': 'en',
'basics-timezone': 'Europe/Berlin',
'basics-presale_start': '2016-11-01 10:00:00',
'basics-presale_end': '2016-11-30 18:00:00',
})
assert doc.select(".alert-danger")
def test_create_duplicate_slug(self):
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga1.pk,
'foundation-locales': ('de', 'en')
})
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'basics',
'basics-name_0': '33C3',
'basics-name_1': '33C3',
'basics-slug': '31c3',
'basics-date_from': '2016-12-27 10:00:00',
'basics-date_to': '2016-12-30 19:00:00',
'basics-location_0': 'Hamburg',
'basics-location_1': 'Hamburg',
'basics-currency': 'EUR',
'basics-locale': 'en',
'basics-timezone': 'Europe/Berlin',
'basics-presale_start': '2016-11-01 10:00:00',
'basics-presale_end': '2016-11-30 18:00:00',
})
assert doc.select(".alert-danger")
def test_create_event_success(self):
doc = self.get_doc('/control/events/add')
tabletext = doc.select("form")[0].text
self.assertIn("CCC", tabletext)
self.assertNotIn("MRM", tabletext)
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga1.pk,
'foundation-locales': ('en', 'de')
})
assert doc.select("#id_basics-name_0")
assert doc.select("#id_basics-name_1")
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'basics',
'basics-name_0': '33C3',
'basics-name_1': '33C3',
'basics-slug': '33c3',
'basics-date_from': '2016-12-27 10:00:00',
'basics-date_to': '2016-12-30 19:00:00',
'basics-location_0': 'Hamburg',
'basics-location_1': 'Hamburg',
'basics-currency': 'EUR',
'basics-locale': 'en',
'basics-timezone': 'Europe/Berlin',
'basics-presale_start': '2016-11-01 10:00:00',
'basics-presale_end': '2016-11-30 18:00:00',
})
assert doc.select("#id_copy-copy_from_event_1")
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'copy',
'copy-copy_from_event': ''
})
ev = Event.objects.get(slug='33c3')
assert ev.name == LazyI18nString({'de': '33C3', 'en': '33C3'})
assert ev.settings.locales == ['en', 'de']
assert ev.settings.locale == 'en'
assert ev.currency == 'EUR'
assert ev.settings.timezone == 'Europe/Berlin'
assert ev.organizer == self.orga1
assert ev.location == LazyI18nString({'de': 'Hamburg', 'en': 'Hamburg'})
assert EventPermission.objects.filter(event=ev, user=self.user).exists()
berlin_tz = timezone('Europe/Berlin')
assert ev.date_from == berlin_tz.localize(datetime.datetime(2016, 12, 27, 10, 0, 0)).astimezone(pytz.utc)
assert ev.date_to == berlin_tz.localize(datetime.datetime(2016, 12, 30, 19, 0, 0)).astimezone(pytz.utc)
assert ev.presale_start == berlin_tz.localize(datetime.datetime(2016, 11, 1, 10, 0, 0)).astimezone(pytz.utc)
assert ev.presale_end == berlin_tz.localize(datetime.datetime(2016, 11, 30, 18, 0, 0)).astimezone(pytz.utc)
def test_create_event_only_date_from(self):
# date_to, presale_start & presale_end are optional fields
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga1.pk,
'foundation-locales': 'en'
})
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'basics',
'basics-name_0': '33C3',
'basics-slug': '33c3',
'basics-date_from': '2016-12-27 10:00:00',
'basics-date_to': '',
'basics-location_0': 'Hamburg',
'basics-currency': 'EUR',
'basics-locale': 'en',
'basics-timezone': 'UTC',
'basics-presale_start': '',
'basics-presale_end': '',
})
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'copy',
'copy-copy_from_event': ''
})
ev = Event.objects.get(slug='33c3')
assert ev.name == LazyI18nString({'en': '33C3'})
assert ev.settings.locales == ['en']
assert ev.settings.locale == 'en'
assert ev.currency == 'EUR'
assert ev.settings.timezone == 'UTC'
assert ev.organizer == self.orga1
assert ev.location == LazyI18nString({'en': 'Hamburg'})
assert EventPermission.objects.filter(event=ev, user=self.user).exists()
assert ev.date_from == datetime.datetime(2016, 12, 27, 10, 0, 0, tzinfo=pytz.utc)
assert ev.date_to is None
assert ev.presale_start is None
assert ev.presale_end is None
def test_create_event_missing_date_from(self):
# date_from is mandatory
self.post_doc('/control/events/add', {
'event_wizard-current_step': 'foundation',
'foundation-organizer': self.orga1.pk,
'foundation-locales': 'en'
})
doc = self.post_doc('/control/events/add', {
'event_wizard-current_step': 'basics',
'basics-name_0': '33C3',
'basics-slug': '33c3',
'basics-date_from': '',
'basics-date_to': '2016-12-30 19:00:00',
'basics-location_0': 'Hamburg',
'basics-currency': 'EUR',
'basics-locale': 'en',
'basics-timezone': 'Europe/Berlin',
'basics-presale_start': '2016-11-20 11:00:00',
'basics-presale_end': '2016-11-24 18:00:00',
})
assert doc.select(".alert-danger")