diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index e7f2076875..b8b2d570fd 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -9,7 +9,9 @@ from django.utils.timezone import get_current_timezone_name from django.utils.translation import pgettext_lazy, ugettext_lazy as _ from django_countries import Countries from django_countries.fields import LazyTypedChoiceField -from i18nfield.forms import I18nFormField, I18nTextarea +from i18nfield.forms import ( + I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, +) from pytz import common_timezones, timezone from pretix.base.forms import I18nModelForm, PlaceholderValidator, SettingsForm @@ -21,6 +23,7 @@ from pretix.control.forms import ( SplitDateTimePickerWidget, ) from pretix.multidomain.urlreverse import build_absolute_uri +from pretix.plugins.banktransfer.payment import BankTransfer from pretix.presale.style import get_fonts @@ -362,6 +365,8 @@ class EventSettingsForm(SettingsForm): ) imprint_url = forms.URLField( label=_("Imprint URL"), + help_text=_("This should point e.g. to a part of your website that has your contact details and legal " + "information."), required=False, ) confirm_text = I18nFormField( @@ -375,7 +380,7 @@ class EventSettingsForm(SettingsForm): contact_mail = forms.EmailField( label=_("Contact address"), required=False, - help_text=_("Public email address for contacting the organizer") + help_text=_("We'll show this publicly to allow attendees to contact you.") ) cancel_allow_user = forms.BooleanField( label=_("Allow users to cancel unpaid orders"), @@ -1039,3 +1044,118 @@ class EventDeleteForm(forms.Form): code='slug_wrong', ) return slug + + +class QuickSetupForm(I18nForm): + show_quota_left = forms.BooleanField( + label=_("Show number of tickets left"), + help_text=_("Publicly show how many tickets of a certain type are still available."), + required=False + ) + waiting_list_enabled = forms.BooleanField( + label=_("Waiting list"), + help_text=_("Once a ticket is sold out, people can add themselves to a waiting list. As soon as a ticket " + "becomes available again, it will be reserved for the first person on the waiting list and this " + "person will receive an email notification with a voucher that can be used to buy a ticket."), + required=False + ) + ticket_download = forms.BooleanField( + label=_("Ticket downloads"), + help_text=_("Your customers will be able to download their tickets in PDF format."), + required=False + ) + attendee_names_required = forms.BooleanField( + label=_("Require all attendees to fill in their names"), + help_text=_("By default, we will ask for names but not require them. You can turn this off completely in the " + "settings."), + required=False + ) + imprint_url = forms.URLField( + label=_("Imprint URL"), + help_text=_("This should point e.g. to a part of your website that has your contact details and legal " + "information."), + required=False, + ) + contact_mail = forms.EmailField( + label=_("Contact address"), + required=False, + help_text=_("We'll show this publicly to allow attendees to contact you.") + ) + total_quota = forms.IntegerField( + label=_("Total capacity"), + min_value=0, + widget=forms.NumberInput( + attrs={ + 'placeholder': '∞' + } + ), + required=False + ) + payment_stripe__enabled = forms.BooleanField( + label=_("Payment via Stripe"), + help_text=_("Stripe is an online payments processor supporting credit cards and lots of other payment options. " + "To accept payments via Stripe, you will need to set up an account with them, which takes less " + "than five minutes using their simple interface."), + required=False + ) + payment_banktransfer__enabled = forms.BooleanField( + label=_("Payment by bank transfer"), + help_text=_("Your customers will be instructed to wire the money to your account. You can then import your " + "bank statements to process the payments within pretix, or mark them as paid manually."), + required=False + ) + payment_banktransfer_bank_details = BankTransfer.form_field(required=False) + + def __init__(self, *args, **kwargs): + self.obj = kwargs.pop('event', None) + self.locales = self.obj.settings.get('locales') if self.obj else kwargs.pop('locales', None) + kwargs['locales'] = self.locales + super().__init__(*args, **kwargs) + if not self.obj.settings.payment_stripe_connect_client_id: + del self.fields['payment_stripe__enabled'] + self.fields['payment_banktransfer_bank_details'].required = False + + +class QuickSetupProductForm(I18nForm): + name = I18nFormField( + max_length=255, + label=_("Product name"), + widget=I18nTextInput + ) + default_price = forms.DecimalField( + label=_("Price (optional)"), + max_digits=7, decimal_places=2, required=False, + localize=True, + widget=forms.TextInput( + attrs={ + 'placeholder': _('Free') + } + ), + ) + quota = forms.IntegerField( + label=_("Quantity available"), + min_value=0, + widget=forms.NumberInput( + attrs={ + 'placeholder': '∞' + } + ), + initial=100, + required=False + ) + + +class BaseQuickSetupProductFormSet(I18nFormSetMixin, forms.BaseFormSet): + + def __init__(self, *args, **kwargs): + event = kwargs.pop('event', None) + if event: + kwargs['locales'] = event.settings.get('locales') + super().__init__(*args, **kwargs) + + +QuickSetupProductFormSet = formset_factory( + QuickSetupProductForm, + formset=BaseQuickSetupProductFormSet, + can_order=False, can_delete=True, extra=0 +) diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index b956916dd0..6949bc55b2 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -40,6 +40,7 @@ + diff --git a/src/pretix/control/templates/pretixcontrol/event/quick_setup.html b/src/pretix/control/templates/pretixcontrol/event/quick_setup.html new file mode 100644 index 0000000000..f6c23f8dd7 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/event/quick_setup.html @@ -0,0 +1,198 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load formset_tags %} +{% block title %}{{ request.event.name }}{% endblock %} +{% block content %} +
+ {% trans "You just created an event!" %} +
++ {% blocktrans trimmed %} + You can scroll down and create your first ticket products quickly, or you can use the navigation + on the left to modify the settings of your event in much more detail. + {% endblocktrans %} +
+ +{}
" - "{}" + "{}" ).format( _('To accept payments via Stripe, you will need an account at Stripe. By clicking on the ' 'following button, you can either create a new Stripe account connect pretix to an existing ' 'one.'), - self.settings.connect_client_id, - request.session['payment_stripe_oauth_token'], - urlquote(build_global_uri('plugins:stripe:oauth.return')), + self.get_connect_url(request), _('Connect with Stripe') ) else: diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index c24c1c9fbf..141c8ab82f 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -93,6 +93,10 @@ def oauth_return(request, *args, **kwargs): event.settings.payment_stripe_connect_user_id = data['stripe_user_id'] event.settings.payment_stripe_connect_user_name = account['business_name'] + if request.session.get('payment_stripe_oauth_enable', False): + event.settings.payment_stripe__enabled = True + del request.session['payment_stripe_oauth_enable'] + return redirect(reverse('control:event.settings.payment.provider', kwargs={ 'organizer': event.organizer.slug, 'event': event.slug, diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index 500e7b41cd..9e52446348 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -206,15 +206,19 @@ var form_handlers = function (el) { dependency.on("change", update); }); - $("input[data-display-dependency]").each(function () { + $("div[data-display-dependency], input[data-display-dependency]").each(function () { var dependent = $(this), dependency = $($(this).attr("data-display-dependency")), update = function (ev) { var enabled = (dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val(); + var $toggling = dependent; + if (dependent.tagName === "input") { + $toggling = dependent.closest('.form-group'); + } if (ev) { - dependent.closest('.form-group').slideToggle(enabled); + $toggling.stop().slideToggle(enabled); } else { - dependent.closest('.form-group').toggle(enabled); + $toggling.stop().toggle(enabled); } }; update(); diff --git a/src/pretix/static/pretixcontrol/js/ui/quicksetup.js b/src/pretix/static/pretixcontrol/js/ui/quicksetup.js new file mode 100644 index 0000000000..a4213276f5 --- /dev/null +++ b/src/pretix/static/pretixcontrol/js/ui/quicksetup.js @@ -0,0 +1,50 @@ +$(function () { + "use strict"; + + var ticket_type_quota_calculation = function () { + var sum = 0; + $("#ticket-type-formset div[data-formset-form]").each(function () { + if (!$(this).find("input[name$=DELETE]").prop("checked")) { + var val = $(this).find("input[name$=quota]").val(); + if (val === "") { + sum = "∞"; + } else if (sum !== "∞") { + sum += parseInt(val); + } + } + }); + $("#total-capacity").text(sum); + }; + + var toggle_payment = function () { + var any = false; + $("#ticket-type-formset div[data-formset-form]").each(function () { + if (!$(this).find("input[name$=DELETE]").prop("checked")) { + var val = $(this).find("input[name$=default_price]").val(); + if (/.*[1-9].*/.test(val)) { + any = true; + } + } + }); + if ($("#quick-setup-step-payment:visible").length && !any) { + $("#quick-setup-step-payment").stop().slideUp(); + } else if (!$("#quick-setup-step-payment:visible").length && any) { + $("#quick-setup-step-payment").stop().slideDown(); + } + }; + + $("#ticket-type-formset").bind("formAdded", ticket_type_quota_calculation); + $("#ticket-type-formset").on("change keyup keydown keypress", "input", function () { + ticket_type_quota_calculation(); + toggle_payment(); + }); + ticket_type_quota_calculation(); + toggle_payment(); + + $("#total-capacity-edit").click(function () { + $("#id_total_quota").val(parseInt($("#total-capacity").text())); + $("#total-capacity").hide(); + $("#id_total_quota").closest("div").removeClass("sr-only"); + $("#total-capacity-edit").hide(); + }); +}); diff --git a/src/pretix/static/pretixcontrol/scss/_forms.scss b/src/pretix/static/pretixcontrol/scss/_forms.scss index b0bab717e8..7333208566 100644 --- a/src/pretix/static/pretixcontrol/scss/_forms.scss +++ b/src/pretix/static/pretixcontrol/scss/_forms.scss @@ -281,3 +281,18 @@ table td > .checkbox input[type="checkbox"] { .panel-title .radio { margin-left: 20px; } + +.form-quicksetup-total-capacity { + height: 34px; + strong { + line-height: 34px; + } +} +.form-horizontal [data-formset] .form-quicksetup-total-capacity .form-group { + margin: 0; + width: auto; +} +.form-quicksetup-total-capacity .form-control { + width: 100px; + display: inline; +} diff --git a/src/pretix/static/pretixcontrol/scss/main.scss b/src/pretix/static/pretixcontrol/scss/main.scss index eebe950bf0..f5c4578668 100644 --- a/src/pretix/static/pretixcontrol/scss/main.scss +++ b/src/pretix/static/pretixcontrol/scss/main.scss @@ -128,6 +128,10 @@ h1 .btn-sm { padding-top: 20px; } +p.bigger { + font-size: 16px; +} + .helper-display-block { display: block !important; } @@ -468,7 +472,33 @@ body.loading #wrapper { text-decoration: none; } } +.quick-setup-step { + clear: both; + .quick-icon { + float: left; + width: 100px; + .fa-ticket, .fa-check-circle { + margin-top: -20px; + } + .fa-wrench { + margin-top: -30px; + } + .fa-money, .fa-envelope { + margin-top: -40px; + } + } + .quick-icon .fa { + font-size: 100px; + line-height: 170px; + display: block; + } + .quick-content { + margin-left: 160px; + padding-top: 5px; + overflow: hidden; + } +} .thank-you { margin-bottom: 25px; @@ -503,6 +533,16 @@ body.loading #wrapper { } } } +@media (max-width: $screen-md-max) { + .quick-setup-step { + .quick-icon { + display: none; + } + .quick-content { + margin-left: 0; + } + } +} @media (max-width: $screen-sm-max) { .thank-you { text-align: center; diff --git a/src/tests/base/__init__.py b/src/tests/base/__init__.py index cf9882c504..2bfc4affc7 100644 --- a/src/tests/base/__init__.py +++ b/src/tests/base/__init__.py @@ -11,7 +11,10 @@ class SoupTest(TestCase): def post_doc(self, *args, **kwargs): kwargs['follow'] = True response = self.client.post(*args, **kwargs) - return BeautifulSoup(response.rendered_content, "lxml") + try: + return BeautifulSoup(response.rendered_content, "lxml") + except AttributeError: + return BeautifulSoup(response.content, "lxml") def extract_form_fields(soup): @@ -36,7 +39,7 @@ def extract_form_fields(soup): # textareas for textarea in soup.findAll('textarea'): - data[textarea['name']] = textarea.string or '' + data[textarea['name']] = textarea.text or '' # select fields for select in soup.find_all('select'): diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index e08dd4dbba..e4483b32bf 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -48,6 +48,151 @@ class EventsTest(SoupTest): self.assertNotIn("31C3", tabletext) self.assertNotIn("MRMCD14", tabletext) + def test_quick_setup_later(self): + self.event1.quotas.create(name='foo', size=2) + resp = self.client.get('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug)) + self.assertRedirects(resp, '/control/event/%s/%s/' % (self.orga1.slug, self.event1.slug)) + + def test_quick_setup_total_quota(self): + doc = self.get_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug)) + doc.select("[name=show_quota_left]")[0]['checked'] = "checked" + doc.select("[name=ticket_download]")[0]['checked'] = "checked" + doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" + doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" + doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=total_quota]")[0]['value'] = "300" + doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-MIN_NUM_FORMS]")[0]['value'] = "0" + doc.select("[name=form-MAX_NUM_FORMS]")[0]['value'] = "1000" + doc.select("[name=form-0-name_0]")[0]['value'] = "Normal ticket" + doc.select("[name=form-0-default_price]")[0]['value'] = "13.90" + doc.select("[name=form-0-quota]")[0]['value'] = "" + doc.select("[name=form-1-name_0]")[0]['value'] = "Reduced ticket" + doc.select("[name=form-1-default_price]")[0]['value'] = "13.20" + doc.select("[name=form-1-quota]")[0]['value'] = "" + + doc = self.post_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug), + extract_form_fields(doc.select('.container-fluid form')[0])) + assert len(doc.select(".alert-success")) > 0 + self.event1.refresh_from_db() + self.event1.settings.flush() + assert self.event1.settings.show_quota_left + assert self.event1.settings.contact_mail == "test@example.org" + assert self.event1.settings.ticket_download + assert self.event1.settings.ticketoutput_pdf__enabled + assert self.event1.settings.payment_banktransfer__enabled + assert self.event1.settings.get('payment_banktransfer_bank_details', as_type=LazyI18nString).localize('en') == "Foo" + assert 'pretix.plugins.banktransfer' in self.event1.plugins + assert self.event1.items.count() == 2 + i = self.event1.items.first() + assert str(i.name) == "Normal ticket" + assert i.default_price == Decimal('13.90') + i = self.event1.items.last() + assert str(i.name) == "Reduced ticket" + assert i.default_price == Decimal('13.20') + assert self.event1.quotas.count() == 1 + q = self.event1.quotas.first() + assert q.name == 'Tickets' + assert q.size == 300 + assert q.items.count() == 2 + + def test_quick_setup_single_quota(self): + doc = self.get_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug)) + doc.select("[name=show_quota_left]")[0]['checked'] = "checked" + doc.select("[name=ticket_download]")[0]['checked'] = "checked" + doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" + doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" + doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=total_quota]")[0]['value'] = "" + doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-MIN_NUM_FORMS]")[0]['value'] = "0" + doc.select("[name=form-MAX_NUM_FORMS]")[0]['value'] = "1000" + doc.select("[name=form-0-name_0]")[0]['value'] = "Normal ticket" + doc.select("[name=form-0-default_price]")[0]['value'] = "13.90" + doc.select("[name=form-0-quota]")[0]['value'] = "100" + doc.select("[name=form-1-name_0]")[0]['value'] = "Reduced ticket" + doc.select("[name=form-1-default_price]")[0]['value'] = "13.20" + doc.select("[name=form-1-quota]")[0]['value'] = "50" + + doc = self.post_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug), + extract_form_fields(doc.select('.container-fluid form')[0])) + assert len(doc.select(".alert-success")) > 0 + self.event1.refresh_from_db() + self.event1.settings.flush() + assert self.event1.settings.show_quota_left + assert self.event1.settings.contact_mail == "test@example.org" + assert self.event1.settings.ticket_download + assert self.event1.settings.ticketoutput_pdf__enabled + assert self.event1.settings.payment_banktransfer__enabled + assert self.event1.settings.get('payment_banktransfer_bank_details', as_type=LazyI18nString).localize('en') == "Foo" + assert 'pretix.plugins.banktransfer' in self.event1.plugins + assert self.event1.items.count() == 2 + i = self.event1.items.first() + assert str(i.name) == "Normal ticket" + assert i.default_price == Decimal('13.90') + i = self.event1.items.last() + assert str(i.name) == "Reduced ticket" + assert i.default_price == Decimal('13.20') + assert self.event1.quotas.count() == 2 + q = self.event1.quotas.first() + assert q.name == 'Normal ticket' + assert q.size == 100 + assert q.items.count() == 1 + q = self.event1.quotas.last() + assert q.name == 'Reduced ticket' + assert q.size == 50 + assert q.items.count() == 1 + + def test_quick_setup_dual_quota(self): + doc = self.get_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug)) + doc.select("[name=show_quota_left]")[0]['checked'] = "checked" + doc.select("[name=ticket_download]")[0]['checked'] = "checked" + doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" + doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" + doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=total_quota]")[0]['value'] = "120" + doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" + doc.select("[name=form-MIN_NUM_FORMS]")[0]['value'] = "0" + doc.select("[name=form-MAX_NUM_FORMS]")[0]['value'] = "1000" + doc.select("[name=form-0-name_0]")[0]['value'] = "Normal ticket" + doc.select("[name=form-0-default_price]")[0]['value'] = "13.90" + doc.select("[name=form-0-quota]")[0]['value'] = "100" + doc.select("[name=form-1-name_0]")[0]['value'] = "Reduced ticket" + doc.select("[name=form-1-default_price]")[0]['value'] = "13.20" + doc.select("[name=form-1-quota]")[0]['value'] = "50" + + doc = self.post_doc('/control/event/%s/%s/quickstart/' % (self.orga1.slug, self.event1.slug), + extract_form_fields(doc.select('.container-fluid form')[0])) + assert len(doc.select(".alert-success")) > 0 + self.event1.refresh_from_db() + self.event1.settings.flush() + assert self.event1.settings.show_quota_left + assert self.event1.settings.contact_mail == "test@example.org" + assert self.event1.settings.ticket_download + assert self.event1.settings.ticketoutput_pdf__enabled + assert self.event1.settings.payment_banktransfer__enabled + assert self.event1.settings.get('payment_banktransfer_bank_details', as_type=LazyI18nString).localize('en') == "Foo" + assert 'pretix.plugins.banktransfer' in self.event1.plugins + assert self.event1.items.count() == 2 + i = self.event1.items.first() + assert str(i.name) == "Normal ticket" + assert i.default_price == Decimal('13.90') + i = self.event1.items.last() + assert str(i.name) == "Reduced ticket" + assert i.default_price == Decimal('13.20') + assert self.event1.quotas.count() == 3 + q = self.event1.quotas.first() + assert q.name == 'Normal ticket' + assert q.size == 100 + assert q.items.count() == 1 + q = self.event1.quotas.last() + assert q.name == 'Tickets' + assert q.size == 120 + assert q.items.count() == 2 + def test_settings(self): doc = self.get_doc('/control/event/%s/%s/settings/' % (self.orga1.slug, self.event1.slug)) doc.select("[name=date_to_0]")[0]['value'] = "2013-12-30"