Add quick-start assistant for new users (#833)

* First draft for quick-setup

* Add payment

* Fix stripe w/o connect

* cols

* Add tests
This commit is contained in:
Raphael Michel
2018-03-26 20:52:24 +02:00
committed by GitHub
parent d578dedd0c
commit 28506538a3
16 changed files with 781 additions and 40 deletions

View File

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

View File

@@ -40,6 +40,7 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/question.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/mail.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/typeahead.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asyncdownload.js" %}"></script>
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>

View File

@@ -0,0 +1,198 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<div class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-check-circle text-success"></span>
</div>
<div class="quick-content">
<h2>{% trans "Congratulations!" %}</h2>
<p>
<strong>{% trans "You just created an event!" %}</strong>
</p>
<p>
{% 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 %}
</p>
<div class="clearfix"></div>
</div>
</div>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-ticket text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Create ticket types" %}</legend>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div class="row hidden-sm hidden-xs" data-formset-form>
<div class="col-md-6">
<strong>{% trans "Ticket name" %}</strong>
</div>
<div class="col-md-3">
<strong>{% trans "Price (optional)" %}</strong>
</div>
<div class="col-md-2">
<strong>{% trans "Capacity (optional)" %}</strong>
</div>
</div>
<div data-formset-body id="ticket-type-formset">
{% for iform in formset %}
<div class="row question-option-row" data-formset-form>
<div class="sr-only">
{{ iform.id }}
{% bootstrap_field iform.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-6">
{% bootstrap_form_errors iform %}
{% bootstrap_field iform.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field iform.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field iform.quota layout='inline' form_group_class="" %}
</div>
<div class="col-md-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="row question-option-row" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="col-md-6">
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
</div>
<div class="col-md-3">
{% bootstrap_field formset.empty_form.default_price addon_after=request.event.currency layout='inline' form_group_class="" %}
</div>
<div class="col-md-2">
{% bootstrap_field formset.empty_form.quota layout='inline' form_group_class="" %}
</div>
<div class="col-md-1 text-right">
<button type="button" class="btn btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
{% endescapescript %}
</script>
<div class="row question-option-row helper-width-100">
<div class="col-md-6">
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a new ticket type" %}</button>
</div>
<div class="col-md-3">
</div>
<div class="col-md-3 form-inline form-quicksetup-total-capacity">
<strong>{% trans "Total capacity:" %}</strong>
<span id="total-capacity"></span>
{% bootstrap_field form.total_quota layout="inline" field_class="sr-only" %}
<a href="#" data-toggle="tooltip" title="{% trans 'You can set a limit on the total number of tickets sold for your event, regardless of the ticket type.' %}" id="total-capacity-edit">
<span class="fa fa-edit"></span>
</a>
</div>
</div>
</div>
<p>&nbsp;</p>
<p class="bigger">
{% blocktrans trimmed %}
If you want to use more advanced features like non-admission products, product variations, custom
quotas, add-on products or want to modify your ticket types in more detail, you can later do so
in the "Products" section in the navigation. Don't worry, you can change everything you input here.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-wrench text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Features" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
We recommend that you take some time to go through the "Settings" part of your event, but if
you're in a hurry and want to get started quickly, here's a short version:
{% endblocktrans %}
</p>
{% bootstrap_field form.ticket_download layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.waiting_list_enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.show_quota_left layout="control" label_class="sr-only" field_class="col-md-12" %}
{% bootstrap_field form.attendee_names_required layout="control" label_class="sr-only" field_class="col-md-12" %}
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step" id="quick-setup-step-payment">
<div class="quick-icon">
<span class="fa fa-fw fa-money text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Payment" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
pretix supports a
<a href="https://pretix.eu/about/en/features/payment" target="_blank">wide range of payment
providers</a> allowing you to choose the payment methods that fit your workflow best.
Here are just two of them as examples, you can add more in the "Settings" part of your event.
{% endblocktrans %}
</p>
{% bootstrap_field form.payment_banktransfer__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
<div data-display-dependency="#id_payment_banktransfer__enabled">
{% bootstrap_field form.payment_banktransfer_bank_details layout="control" %}
</div>
{% if form.payment_stripe__enabled %}
{% bootstrap_field form.payment_stripe__enabled layout="control" label_class="sr-only" field_class="col-md-12" %}
<div data-display-dependency="#id_payment_stripe__enabled">
<div class="alert alert-info">
{% blocktrans trimmed %}
After you saved this page, we will redirect you to Stripe to create or connect an account
there. Once you completed this, you will be taken back to pretix.
{% endblocktrans %}
</div>
</div>
{% endif %}
<p>&nbsp;</p>
</div>
</fieldset>
<fieldset class="quick-setup-step">
<div class="quick-icon">
<span class="fa fa-fw fa-envelope text-muted"></span>
</div>
<div class="quick-content">
<legend>{% trans "Getting in touch with you" %}</legend>
<p class="bigger">
{% blocktrans trimmed %}
In case something goes wrong or is unclear, we strongly suggest that you provide ways for your
attendees to contact you:
{% endblocktrans %}
</p>
{% bootstrap_field form.contact_mail layout="control" %}
{% bootstrap_field form.imprint_url layout="control" %}
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -73,6 +73,7 @@ urlpatterns = [
name='event.requiredaction.discard'),
url(r'^comment/$', event.EventComment.as_view(),
name='event.comment'),
url(r'^quickstart/$', event.QuickSetupView.as_view(), name='event.quick'),
url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'),
url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'),
url(r'^settings/permissions$', event.EventPermissions.as_view(), name='event.settings.permissions'),

View File

@@ -220,20 +220,11 @@ def welcome_wizard_widget(sender, **kwargs):
if not sender.items.exists():
ctx.update({
'subtitle': _('Get started by creating a product'),
'text': _('The first thing you need for selling tickets to your event is one or more "products" your '
'participants can choose from. A product can be a ticket or anything else that you want to sell, '
'e.g. additional merchandise in form of t-shirts.'),
'button_text': _('Create a first product'),
'button_url': reverse('control:event.items.add', kwargs=kwargs)
})
elif not sender.quotas.exists():
ctx.update({
'subtitle': _('Create quotas that apply to your products'),
'text': _('Your tickets will only be available for sale if you create a matching quota, i.e. if you tell '
'pretix how many tickets it should sell for your event.'),
'button_text': _('Create a first quota'),
'button_url': reverse('control:event.items.quotas.add', kwargs=kwargs)
'subtitle': _('Get started with our setup tool'),
'text': _('To start selling tickets, you need to create products or quotas. The fastest way to create '
'this is to use our setup tool.'),
'button_text': _('Set up event'),
'button_url': reverse('control:event.quick', kwargs=kwargs)
})
else:
return []

View File

@@ -41,13 +41,15 @@ from pretix.base.templatetags.money import money_filter
from pretix.control.forms.event import (
CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
PaymentSettingsForm, ProviderForm, TaxRuleForm, TaxRuleLineFormSet,
PaymentSettingsForm, ProviderForm, QuickSetupForm,
QuickSetupProductFormSet, TaxRuleForm, TaxRuleLineFormSet,
TicketSettingsForm, WidgetCodeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.control.signals import nav_event_settings
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import get_domain
from pretix.plugins.stripe.payment import StripeSettingsHolder
from pretix.presale.style import regenerate_css
from . import CreateView, PaginationMixin, UpdateView
@@ -1120,3 +1122,154 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
domain = '%s:%d' % (domain, siteurlsplit.port)
ctx['urlprefix'] = '%s://%s' % (siteurlsplit.scheme, domain)
return ctx
class QuickSetupView(FormView):
template_name = 'pretixcontrol/event/quick_setup.html'
permission = 'can_change_event_settings'
form_class = QuickSetupForm
def dispatch(self, request, *args, **kwargs):
if request.event.items.exists() or request.event.quotas.exists():
messages.info(request, _('Your event is not empty, you need to set it up manually.'))
return redirect(reverse('control:event.index', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug
}))
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
return kwargs
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['formset'] = self.formset
return ctx
def get_initial(self):
return {
'waiting_list_enabled': True,
'ticket_download': True,
'contact_mail': self.request.event.settings.contact_mail,
'imprint_url': self.request.event.settings.imprint_url,
}
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid() and self.formset.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
@transaction.atomic
def form_valid(self, form):
plugins_active = self.request.event.get_plugins()
if form.cleaned_data['ticket_download']:
if 'pretix.plugins.ticketoutputpdf' not in plugins_active:
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': 'pretix.plugins.ticketoutputpdf'})
plugins_active.append('pretix.plugins.ticketoutputpdf')
self.request.event.settings.ticket_download = True
self.request.event.settings.ticketoutput_pdf__enabled = True
if form.cleaned_data['payment_banktransfer__enabled']:
if 'pretix.plugins.banktransfer' not in plugins_active:
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': 'pretix.plugins.banktransfer'})
plugins_active.append('pretix.plugins.banktransfer')
self.request.event.settings.payment_banktransfer__enabled = True
self.request.event.settings.payment_banktransfer_bank_details = form.cleaned_data['payment_banktransfer_bank_details']
if form.cleaned_data.get('payment_stripe__enabled', None):
if 'pretix.plugins.stripe' not in plugins_active:
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
data={'plugin': 'pretix.plugins.stripe'})
plugins_active.append('pretix.plugins.stripe')
self.request.event.settings.show_quota_left = form.cleaned_data['show_quota_left']
self.request.event.settings.waiting_list_enabled = form.cleaned_data['waiting_list_enabled']
self.request.event.settings.attendee_names_required = form.cleaned_data['attendee_names_required']
self.request.event.settings.contact_mail = form.cleaned_data['contact_mail']
self.request.event.settings.imprint_url = form.cleaned_data['imprint_url']
self.request.event.log_action('pretix.event.settings', user=self.request.user, data={
k: self.request.event.settings.get(k) for k in form.changed_data
})
items = []
category = None
tax_rule = self.request.event.tax_rules.first()
if any(f not in self.formset.deleted_forms for f in self.formset):
category = self.request.event.categories.create(
name=LazyI18nString.from_gettext(ugettext('Tickets'))
)
category.log_action('pretix.event.category.added', data={'name': ugettext('Tickets')},
user=self.request.user)
for i, f in enumerate(self.formset):
if f in self.formset.deleted_forms:
continue
item = self.request.event.items.create(
name=f.cleaned_data['name'],
category=category,
active=True,
default_price=f.cleaned_data['default_price'] or 0,
tax_rule=tax_rule,
admission=True,
position=i,
)
item.log_action('pretix.event.item.added', user=self.request.user, data=dict(f.cleaned_data))
if f.cleaned_data['quota'] or not form.cleaned_data['total_quota']:
quota = self.request.event.quotas.create(
name=str(f.cleaned_data['name']),
size=f.cleaned_data['quota'],
)
quota.log_action('pretix.event.quota.added', user=self.request.user, data=dict(f.cleaned_data))
quota.items.add(item)
items.append(item)
if form.cleaned_data['total_quota']:
quota = self.request.event.quotas.create(
name=ugettext('Tickets'),
size=form.cleaned_data['total_quota']
)
quota.log_action('pretix.event.quota.added', user=self.request.user, data={
'name': ugettext('Tickets'),
'size': quota.size
})
quota.items.add(*items)
self.request.event.plugins = ",".join(plugins_active)
self.request.event.save()
messages.success(self.request, _('Your changes have been saved. You can now go on with looking at the details '
'or take your event live to start selling!'))
if form.cleaned_data.get('payment_stripe__enabled', False):
self.request.session['payment_stripe_oauth_enable'] = True
return redirect(StripeSettingsHolder(self.request.event).get_connect_url(self.request))
return redirect(reverse('control:event.index', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}))
@cached_property
def formset(self):
return QuickSetupProductFormSet(
data=self.request.POST if self.request.method == "POST" else None,
event=self.request.event,
initial=[
{
'name': LazyI18nString.from_gettext(ugettext('Regular ticket')),
'default_price': Decimal('35.00'),
'quota': 100,
},
{
'name': LazyI18nString.from_gettext(ugettext('Reduced ticket')),
'default_price': Decimal('29.00'),
'quota': 50,
},
]
)

View File

@@ -208,10 +208,16 @@ class EventWizard(SessionWizardView):
event.settings.set('locale', basics_data['locale'])
event.settings.set('locales', foundation_data['locales'])
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '?congratulations=1')
if (copy_data and copy_data['copy_from_event']) or event.has_subevents:
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '?congratulations=1')
else:
return redirect(reverse('control:event.quick', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,
}) + '?congratulations=1')
class SlugRNG(OrganizerPermissionRequiredMixin, View):

View File

@@ -14,9 +14,9 @@ class BankTransfer(BasePaymentProvider):
identifier = 'banktransfer'
verbose_name = _('Bank transfer')
@property
def settings_form_fields(self):
form_field = I18nFormField(
@staticmethod
def form_field(**kwargs):
return I18nFormField(
label=_('Bank account details'),
widget=I18nTextarea,
help_text=_('Include everything that your customers need to send you a bank transfer payment. Within SEPA '
@@ -30,10 +30,14 @@ class BankTransfer(BasePaymentProvider):
'Account owner: John Doe\n'
'Name of Bank: Professional Banking Institute Ltd., London'
)
}}
}},
**kwargs
)
@property
def settings_form_fields(self):
d = OrderedDict(
list(super().settings_form_fields.items()) + [('bank_details', form_field)]
list(super().settings_form_fields.items()) + [('bank_details', self.form_field())]
)
d.move_to_end('bank_details', last=False)
d.move_to_end('_enabled', last=False)

View File

@@ -51,25 +51,31 @@ class StripeSettingsHolder(BasePaymentProvider):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'stripe', event)
def get_connect_url(self, request):
request.session['payment_stripe_oauth_event'] = request.event.pk
if 'payment_stripe_oauth_token' not in request.session:
request.session['payment_stripe_oauth_token'] = get_random_string(32)
return (
"https://connect.stripe.com/oauth/authorize?response_type=code&client_id={}&state={}"
"&scope=read_write&redirect_uri={}"
).format(
self.settings.connect_client_id,
request.session['payment_stripe_oauth_token'],
urlquote(build_global_uri('plugins:stripe:oauth.return')),
)
def settings_content_render(self, request):
if self.settings.connect_client_id and not self.settings.secret_key:
# Use Stripe connect
if not self.settings.connect_user_id:
request.session['payment_stripe_oauth_event'] = request.event.pk
if 'payment_stripe_oauth_token' not in request.session:
request.session['payment_stripe_oauth_token'] = get_random_string(32)
return (
"<p>{}</p>"
"<a href='https://connect.stripe.com/oauth/authorize?response_type=code&client_id={}&state={}"
"&scope=read_write&redirect_uri={}' class='btn btn-primary btn-lg'>{}</a>"
"<a href='{}' class='btn btn-primary btn-lg'>{}</a>"
).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:

View File

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

View File

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

View File

@@ -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();
});
});

View File

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

View File

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

View File

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

View File

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