Separate personalization from admission (#2990)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-01-09 14:57:35 +01:00
committed by GitHub
parent e5528f7784
commit 603225d042
30 changed files with 293 additions and 52 deletions

View File

@@ -234,7 +234,7 @@ class ItemSerializer(I18nAwareModelSerializer):
class Meta:
model = Item
fields = ('id', 'category', 'name', 'internal_name', 'active', 'sales_channels', 'description',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission', 'personalized',
'position', 'picture', 'available_from', 'available_until',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
@@ -262,6 +262,15 @@ class ItemSerializer(I18nAwareModelSerializer):
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
if data.get('personalized') and not data.get('admission'):
raise ValidationError(_('Only admission products can currently be personalized.'))
if data.get('admission') and 'personalized' not in data and not self.instance:
# Backwards compatibility
data['personalized'] = True
elif not data.get('admission'):
data['personalized'] = False
if data.get('issue_giftcard'):
if data.get('tax_rule') and data.get('tax_rule').rate > 0:
raise ValidationError(

View File

@@ -73,6 +73,7 @@ class ItemDataExporter(ListExporter):
_("Free price input"),
_("Sales tax"),
_("Is an admission ticket"),
_("Personalized ticket"),
_("Generate tickets"),
_("Waiting list"),
_("Available from"),
@@ -144,6 +145,7 @@ class ItemDataExporter(ListExporter):
_("Yes") if i.free_price else "",
str(i.tax_rule) if i.tax_rule else "",
_("Yes") if i.admission else "",
_("Yes") if i.personalized else "",
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
_("Yes") if i.allow_waitinglist else "",
date_format(_max(i.available_from, v.available_from).astimezone(self.timezone),

View File

@@ -78,6 +78,7 @@ class JSONExporter(BaseExporter):
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission,
'personalized': item.personalized,
'active': item.active,
'sales_channels': item.sales_channels,
'description': str(item.description),

View File

@@ -575,7 +575,7 @@ class BaseQuestionsForm(forms.Form):
add_fields = {}
if item.admission and event.settings.attendee_names_asked:
if item.ask_attendee_data and event.settings.attendee_names_asked:
add_fields['attendee_name_parts'] = NamePartsFormField(
max_length=255,
required=event.settings.attendee_names_required and not self.all_optional,
@@ -584,7 +584,7 @@ class BaseQuestionsForm(forms.Form):
label=_('Attendee name'),
initial=(cartpos.attendee_name_parts if cartpos else orderpos.attendee_name_parts),
)
if item.admission and event.settings.attendee_emails_asked:
if item.ask_attendee_data and event.settings.attendee_emails_asked:
add_fields['attendee_email'] = forms.EmailField(
required=event.settings.attendee_emails_required and not self.all_optional,
label=_('Attendee email'),
@@ -595,7 +595,7 @@ class BaseQuestionsForm(forms.Form):
}
)
)
if item.admission and event.settings.attendee_company_asked:
if item.ask_attendee_data and event.settings.attendee_company_asked:
add_fields['company'] = forms.CharField(
required=event.settings.attendee_company_required and not self.all_optional,
label=_('Company'),
@@ -603,7 +603,7 @@ class BaseQuestionsForm(forms.Form):
initial=(cartpos.company if cartpos else orderpos.company),
)
if item.admission and event.settings.attendee_addresses_asked:
if item.ask_attendee_data and event.settings.attendee_addresses_asked:
add_fields['street'] = forms.CharField(
required=event.settings.attendee_addresses_required and not self.all_optional,
label=_('Address'),

View File

@@ -0,0 +1,27 @@
# Generated by Django 3.2.16 on 2022-12-21 08:59
from django.db import migrations, models
def item_set_personalized(apps, schema_editor):
# We cannot really know if a position was bundled or an add-on, but we can at least guess
Item = apps.get_model("pretixbase", "Item")
Item.objects.filter(admission=True).update(personalized=True)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0226_itemvariationmetavalue'),
]
operations = [
migrations.AddField(
model_name='item',
name='personalized',
field=models.BooleanField(default=False),
),
migrations.RunPython(
item_set_personalized,
migrations.RunPython.noop,
),
]

View File

@@ -310,6 +310,8 @@ class Item(LoggedModel):
:type tax_rate: decimal.Decimal
:param admission: ``True``, if this item allows persons to enter the event (as opposed to e.g. merchandise)
:type admission: bool
:param personalized: ``True``, if attendee information should be collected for this ticket
:type personalized: bool
:param picture: A product picture to be shown next to the product description
:type picture: File
:param available_from: The date this product goes on sale
@@ -396,8 +398,14 @@ class Item(LoggedModel):
admission = models.BooleanField(
verbose_name=_("Is an admission ticket"),
help_text=_(
'Whether or not buying this product allows a person to enter '
'your event'
'Whether or not buying this product allows a person to enter your event'
),
default=False
)
personalized = models.BooleanField(
verbose_name=_("Is a personalized ticket"),
help_text=_(
'Whether or not buying this product allows to enter attendee information'
),
default=False
)
@@ -578,6 +586,10 @@ class Item(LoggedModel):
return self.event.settings.show_quota_left
return self.show_quota_left
@property
def ask_attendee_data(self):
return self.admission and self.personalized
def tax(self, price=None, base_price_is='auto', currency=None, invoice_address=None, override_tax_rate=None, include_bundled=False):
price = price if price is not None else self.default_price

View File

@@ -808,7 +808,7 @@ class Order(LockModel, LoggedModel):
return True
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
for cp in positions:
if (cp.item.admission and ask_names) or cp.item.questions.all():
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
return True
return False # nothing there to modify

View File

@@ -206,7 +206,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for attendee names"),
help_text=_("Ask for a name for all tickets which include admission to the event."),
help_text=_("Ask for a name for all personalized tickets."),
)
},
'attendee_names_required': {
@@ -229,10 +229,10 @@ DEFAULTS = {
label=_("Ask for email addresses per ticket"),
help_text=_("Normally, pretix asks for one email address per order and the order confirmation will be sent "
"only to that email address. If you enable this option, the system will additionally ask for "
"individual email addresses for every admission ticket. This might be useful if you want to "
"individual email addresses for every personalized ticket. This might be useful if you want to "
"obtain individual addresses for every attendee even in case of group orders. However, "
"pretix will send the order confirmation by default only to the one primary email address, not to "
"the per-attendee addresses. You can however enable this in the E-mail settings."),
"the per-attendee addresses. You can however enable this in the email settings."),
)
},
'attendee_emails_required': {
@@ -242,7 +242,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require email addresses per ticket"),
help_text=_("Require customers to fill in individual e-mail addresses for all admission tickets. See the "
help_text=_("Require customers to fill in individual email addresses for all personalized tickets. See the "
"above option for more details. One email address for the order confirmation will always be "
"required regardless of this setting."),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_settings-attendee_emails_asked'}),
@@ -2574,7 +2574,7 @@ Your {organizer} team"""))
label=_("Attendee data explanation"),
widget=I18nTextarea,
widget_kwargs={'attrs': {'rows': '2'}},
help_text=_("This text will be shown above the questions asked for every admission product. You can use it e.g. to explain "
help_text=_("This text will be shown above the questions asked for every personalized product. You can use it e.g. to explain "
"why you need information from them.")
)
},

View File

@@ -78,7 +78,7 @@ class BaseQuestionsViewMixin:
form.pos = cartpos or orderpos
form.show_copy_answers_to_addon_button = form.pos.addon_to and (
set(form.pos.addon_to.item.questions.all()) & set(form.pos.item.questions.all()) or
(form.pos.addon_to.item.admission and form.pos.item.admission and (
(form.pos.addon_to.item.ask_attendee_data and form.pos.item.ask_attendee_data and (
self.request.event.settings.attendee_names_asked or
self.request.event.settings.attendee_emails_asked or
self.request.event.settings.attendee_company_asked or

View File

@@ -295,6 +295,7 @@ class ItemCreateForm(I18nModelForm):
self.user = kwargs.pop('user')
kwargs.setdefault('initial', {})
kwargs['initial'].setdefault('admission', True)
kwargs['initial'].setdefault('personalized', True)
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.instance.event.categories.all()
@@ -403,6 +404,8 @@ class ItemCreateForm(I18nModelForm):
self.instance.sales_channels = list(get_all_sales_channels().keys())
self.instance.position = (self.event.items.aggregate(p=Max('position'))['p'] or 0) + 1
if not self.instance.admission:
self.instance.personalized = False
instance = super().save(*args, **kwargs)
if not self.event.has_subevents and not self.cleaned_data.get('has_variations'):
@@ -494,6 +497,7 @@ class ItemCreateForm(I18nModelForm):
'internal_name',
'category',
'admission',
'personalized',
'default_price',
'tax_rule',
]
@@ -588,13 +592,14 @@ class ItemUpdateForm(I18nModelForm):
'tax_rule',
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
)
if d['admission']:
if d.get('admission'):
self.add_error(
'admission',
_(
"Gift card products should not be admission products at the same time."
)
)
if d.get('require_membership') and not d.get('require_membership_types'):
self.add_error(
'require_membership_types',
@@ -602,6 +607,18 @@ class ItemUpdateForm(I18nModelForm):
"If a valid membership is required, at least one valid membership type needs to be selected."
)
)
if not d.get('admission'):
d['personalized'] = False
if d.get('grant_membership_type'):
if not d['grant_membership_type'].transferable and not d['personalized']:
self.add_error(
'personalized' if d['admission'] else 'admission',
_("Your product grants a non-transferable membership and should therefore be a personalized "
"admission ticket. Otherwise customers might not be able to use the membership later. If you "
"want the membership to be non-personalized, set the membership type to be transferable.")
)
return d
def clean_picture(self):
@@ -622,6 +639,7 @@ class ItemUpdateForm(I18nModelForm):
'active',
'sales_channels',
'admission',
'personalized',
'description',
'picture',
'default_price',

View File

@@ -90,7 +90,7 @@
</div>
</div>
<h4>{% trans "Attendee data (once per admission ticket)" %}</h4>
<h4>{% trans "Attendee data (once per personalized ticket)" %}</h4>
{% bootstrap_field sform.attendee_names_asked_required layout="control" %}
{% bootstrap_field sform.attendee_emails_asked_required layout="control" %}

View File

@@ -20,13 +20,19 @@
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<span class="fa fa-user"></span>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on">
<span class="fa fa-fw fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
By default, we will only offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -40,12 +46,12 @@
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.admission.html_name }}" {% if not form.admission.value %}checked{% endif %}>
<span class="fa fa-cube"></span>
<span class="fa fa-fw fa-cube"></span>
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
A product that does not represent a person. By default, we will not offer ticket downloads
(but you can still enable ticket downloads in event settings or product settings).
{% endblocktrans %}
</div>
<div class="help-block">
@@ -58,6 +64,47 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-circle-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.category layout="control" %}
<div class="form-group">

View File

@@ -27,13 +27,19 @@
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %}>
<input type="radio" value="on" name="{{ form.admission.html_name }}" {% if form.admission.value %}checked{% endif %} id="admission_on">
<span class="fa fa-fw fa-user"></span>
<strong>{% trans "Admission product" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Every purchase of this product represents one person who is allowed to enter your event.
By default, pretix will only ask for attendee information and offer ticket downloads for these products.
By default, we will only offer ticket downloads for these products.
{% endblocktrans %}
</div>
<div class="help-block">
{% blocktrans trimmed %}
Only purchases of such products will be considered "attendees" for most statistical
purposes or within some plugins.
{% endblocktrans %}
</div>
<div class="help-block">
@@ -51,8 +57,8 @@
<strong>{% trans "Non-admission product" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
A product that does not represent a person. By default, pretix will not ask for attendee information or offer
ticket downloads.
A product that does not represent a person. By default, we will not offer ticket downloads
(but you can still enable ticket downloads in event settings or product settings).
{% endblocktrans %}
</div>
<div class="help-block">
@@ -65,6 +71,52 @@
</div>
</div>
<div class="form-group" data-display-dependency="#admission_on">
<label class="col-md-3 control-label">{% trans "Personalization" %}</label>
<div class="col-md-9">
{% for e in form.errors.personalized %}
<div class="alert alert-danger has-error">
{{ e }}
</div>
{% endfor %}
<div class="big-radio radio">
<label>
<input type="radio" value="on" name="{{ form.personalized.html_name }}" {% if form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-id-card-o"></span>
<strong>{% trans "Personalized ticket" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
When this ticket is purchased, the system will ask for a name or other details according
to your event settings.
{% endblocktrans %}
{% if not request.event.settings.attendee_names_asked and not request.event.settings.attendee_emails_asked and not request.event.settings.attendee_company_asked and not request.event.settings.attendee_addresses_asked %}
<br>
<span class="text-warning">
<span class="fa fa-warning" aria-hidden="true"></span>
{% trans "This will currently have no effect since all data fields are turned off in event settings." %}
</span>
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-xs" target="_blank">{% trans "Change settings" %}</a>
{% endif %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" value="" name="{{ form.personalized.html_name }}" {% if not form.personalized.value %}checked{% endif %}>
<span class="fa fa-fw fa-circle-o"></span>
<strong>{% trans "Non-personalized ticket" %}</strong>
<div class="help-block">
{% blocktrans trimmed %}
The system will not ask for a name or other attendee details. This only affects
system-provided fields, you can still add your own questions.
{% endblocktrans %}
</div>
</label>
</div>
</div>
</div>
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.picture layout="control" %}
{% bootstrap_field form.require_approval layout="control" %}

View File

@@ -81,7 +81,11 @@
</td>
<td>
{% if i.admission %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket" %}"></span>
{% if i.personalized %}
<span class="fa fa-id-card-o fa-fw text-muted" data-toggle="tooltip" title="{% trans "Personalized admission ticket" %}"></span>
{% else %}
<span class="fa fa-user fa-fw text-muted" data-toggle="tooltip" title="{% trans "Admission ticket without personalization" %}"></span>
{% endif %}
{% elif i.issue_giftcard %}
<span class="fa fa-gift fa-fw text-muted" data-toggle="tooltip" title="{% trans "Gift card" %}"></span>
{% endif %}

View File

@@ -76,7 +76,7 @@
{% endfor %}
</ul>
{% else %}
<small>{% trans "All admission products" %}</small>
<small>{% trans "All personalized products" %}</small>
{% endif %}
</td>
<td class="dnd-container">

View File

@@ -468,12 +468,12 @@
{% endif %}
{% if line.has_questions %}
<dl>
{% if line.item.admission and event.settings.attendee_names_asked %}
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
<dt>{% trans "Attendee name" %}</dt>
<dd>{% if line.attendee_name %}{{ line.attendee_name }}{% else %}
<em>{% trans "not answered" %}</em>{% endif %}</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_emails_asked %}
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
<dt>{% trans "Attendee email" %}</dt>
<dd>
{% if line.attendee_email %}
@@ -496,7 +496,7 @@
{% endif %}
</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_company_asked %}
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
<dt>
{% trans "Attendee company" %}
</dt>
@@ -504,7 +504,7 @@
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
</dd>
{% endif %}
{% if line.item.admission and event.settings.attendee_addresses_asked %}
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
<dt>
{% trans "Attendee address" %}
</dt>

View File

@@ -1204,7 +1204,7 @@ class ItemCreate(EventPermissionRequiredMixin, CreateView):
initial['tax_rule'] = trs[0]
if self.copy_from:
fields = ('name', 'internal_name', 'category', 'admission', 'default_price', 'tax_rule')
fields = ('name', 'internal_name', 'category', 'admission', 'personalized', 'default_price', 'tax_rule')
for f in fields:
initial[f] = getattr(self.copy_from, f)
initial['copy_from'] = self.copy_from

View File

@@ -383,8 +383,8 @@ class OrderDetail(OrderView):
p.has_questions = (
p.additional_fields or
(p.item.admission and self.request.event.settings.attendee_names_asked) or
(p.item.admission and self.request.event.settings.attendee_emails_asked) or
(p.item.ask_attendee_data and self.request.event.settings.attendee_names_asked) or
(p.item.ask_attendee_data and self.request.event.settings.attendee_emails_asked) or
p.item.questions.all()
)
p.cache_answers()

View File

@@ -85,6 +85,7 @@ PayU
PCI
PDFs
percentual
personalization
POS
postfix
prepended

View File

@@ -985,22 +985,22 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_names_required', as_type=bool) \
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_names_required', as_type=bool) \
and not cp.attendee_name_parts:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
and cp.attendee_email is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_company_required', as_type=bool) \
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_company_required', as_type=bool) \
and cp.company is None:
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))
return False
if cp.item.admission and self.request.event.settings.get('attendee_attendees_required', as_type=bool) \
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_attendees_required', as_type=bool) \
and (cp.street is None or cp.city is None or cp.country is None):
if warn:
messages.warning(request, _('Please fill in answers to all required questions.'))

View File

@@ -97,7 +97,7 @@
</summary>
<div>
<div class="panel-body questions-form">
{% if event.settings.attendee_data_explanation_text and pos.item.admission %}
{% if event.settings.attendee_data_explanation_text and pos.item.ask_attendee_data %}
{{ event.settings.attendee_data_explanation_text|rich_text }}
{% endif %}
{% if pos.seat %}

View File

@@ -127,7 +127,7 @@
{% endif %}
{% if line.has_questions %}
<dl class="dl-indented">
{% if line.item.admission %}
{% if line.item.ask_attendee_data %}
{% if event.settings.attendee_names_asked %}
<dt class="sr-only">
{% trans "Attendee name" %}

View File

@@ -149,7 +149,7 @@ class CartMixin:
# We do this by list manipulations instead of a GROUP BY query, as
# Django is unable to join related models in a .values() query
def group_key(pos): # only used for grouping, sorting is done before already
has_attendee_data = pos.item.admission and (
has_attendee_data = pos.item.ask_attendee_data and (
self.request.event.settings.attendee_names_asked
or self.request.event.settings.attendee_emails_asked
or self.request.event.settings.attendee_company_asked

View File

@@ -262,6 +262,7 @@ TEST_ITEM_RES = {
"tax_rate": "0.00",
"tax_rule": None,
"admission": False,
"personalized": False,
"issue_giftcard": False,
"position": 0,
"generate_tickets": None,
@@ -472,9 +473,12 @@ def test_item_create(token_client, organizer, event, item, category, taxrule, me
)
assert resp.status_code == 201
with scopes_disabled():
assert Item.objects.get(pk=resp.data['id']).sales_channels == ["web", "pretixpos"]
assert Item.objects.get(pk=resp.data['id']).meta_data == {'day': 'Wednesday'}
assert Item.objects.get(pk=resp.data['id']).require_membership_types.count() == 1
i = Item.objects.get(pk=resp.data['id'])
assert i.sales_channels == ["web", "pretixpos"]
assert i.meta_data == {'day': 'Wednesday'}
assert i.require_membership_types.count() == 1
assert i.personalized is True # auto-set for backwards-compatibility
assert i.admission is True
@pytest.mark.django_db
@@ -1077,6 +1081,32 @@ def test_item_update(token_client, organizer, event, item, category, item2, cate
assert resp.status_code == 400
assert resp.content.decode() == '{"meta_data":["Item meta data property \'foo\' does not exist."]}'
item.personalized = True
item.admission = True
item.save()
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
{
"admission": False,
"personalized": True,
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Only admission products can currently be personalized."]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
{
"admission": False
},
format='json'
)
assert resp.status_code == 200
item.refresh_from_db()
assert not item.admission
assert not item.personalized # also set for backwards compatibility
@pytest.mark.django_db
def test_item_file_upload(token_client, organizer, event, item):

View File

@@ -89,7 +89,7 @@ class BaseQuotaTestCase(TestCase):
)
self.quota = Quota.objects.create(name="Test", size=2, event=self.event)
self.item1 = Item.objects.create(event=self.event, name="Ticket", default_price=23,
admission=True)
admission=True, personalized=True)
self.item2 = Item.objects.create(event=self.event, name="T-Shirt", default_price=23)
self.item3 = Item.objects.create(event=self.event, name="Goodie", default_price=23)
self.var1 = ItemVariation.objects.create(item=self.item2, value='S')

View File

@@ -80,7 +80,7 @@ def env():
)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
category=None, default_price=23,
admission=True)
admission=True, personalized=True)
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
OrderPosition.objects.create(
@@ -106,7 +106,7 @@ def test_order_list(client, env):
with scopes_disabled():
otherticket = Item.objects.create(event=env[0], name='Early-bird ticket',
category=None, default_price=23,
admission=True)
admission=True, personalized=True)
client.login(email='dummy@dummy.dummy', password='dummy')
response = client.get('/control/event/dummy/dummy/orders/')
assert 'FOO' in response.content.decode()
@@ -1354,7 +1354,7 @@ class OrderChangeTests(SoupTest):
mtype = self.event.organizer.membership_types.create(name='Week pass', transferable=True, allow_parallel_usage=True)
self.ticket.require_membership = True
self.ticket.require_membership_types.add(mtype)
self.ticket.admission = True
self.ticket.personalized = True
self.ticket.save()
customer = self.event.organizer.customers.create(email='john@example.org', is_verified=True)
self.order.customer = customer

View File

@@ -71,7 +71,7 @@ class BaseCheckoutTestCase:
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
category=self.category, default_price=23, admission=True,
tax_rule=self.tr19)
personalized=True, tax_rule=self.tr19)
self.quota_tickets.items.add(self.ticket)
self.event.settings.set('timezone', 'UTC')
self.event.settings.set('attendee_names_asked', False)
@@ -987,6 +987,31 @@ class CheckoutTestCase(BaseCheckoutTestCase, TestCase):
cr1 = CartPosition.objects.get(id=cr1.id)
self.assertEqual(cr1.attendee_name, 'Peter')
def test_attendee_name_not_required_if_ticket_unpersonalized(self):
self.event.settings.set('attendee_names_asked', True)
self.event.settings.set('attendee_names_required', True)
self.ticket.personalized = False
self.ticket.save()
with scopes_disabled():
cr1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="%s-attendee_name_parts_0"]' % cr1.id)), 0)
# Accepted request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
cr1 = CartPosition.objects.get(id=cr1.id)
self.assertEqual(cr1.attendee_name, None)
def test_attendee_name_scheme(self):
self.event.settings.set('attendee_names_asked', True)
self.event.settings.set('attendee_names_required', True)
@@ -4522,6 +4547,7 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
self.ticket.require_membership = True
self.ticket.require_membership_types.add(mtype)
self.ticket.admission = True
self.ticket.personalized = True
self.ticket.save()
self.event.settings.attendee_names_asked = True

View File

@@ -77,7 +77,7 @@ class BaseOrdersTest(TestCase):
self.quota_tickets = Quota.objects.create(event=self.event, name='Tickets', size=5)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
category=self.category, default_price=23,
admission=True)
admission=True, personalized=True)
self.quota_tickets.items.add(self.ticket)
self.event.settings.set('attendee_names_asked', True)
self.question = Question.objects.create(question='Foo', type=Question.TYPE_STRING, event=self.event,