mirror of
https://github.com/pretix/pretix.git
synced 2025-12-28 17:52:26 +00:00
Compare commits
23 Commits
subevent-b
...
fix-questi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
211804dde7 | ||
|
|
3f3715f80c | ||
|
|
b5e20df508 | ||
|
|
eba5c1b36d | ||
|
|
7d30ecf527 | ||
|
|
2359307462 | ||
|
|
325f7c565d | ||
|
|
df48adef1b | ||
|
|
74cea09f6c | ||
|
|
e8abe5cad8 | ||
|
|
6c9f66487d | ||
|
|
5f828127bf | ||
|
|
c5b3093f20 | ||
|
|
ae4073b3e4 | ||
|
|
362ac8de6f | ||
|
|
cced9cd768 | ||
|
|
dfb45e13ca | ||
|
|
23489f50f8 | ||
|
|
80148a8435 | ||
|
|
9f49b7747c | ||
|
|
b75f8bf893 | ||
|
|
d53af424cf | ||
|
|
24c02751cc |
@@ -1059,9 +1059,10 @@ Creating orders
|
||||
prices. Note that this will not include other fees and is calculated once during order generation and will not
|
||||
be respected automatically when the order changes later.)
|
||||
* ``_split_taxes_like_products`` (Optional convenience flag. If set to ``true``, your ``tax_rule`` will be ignored
|
||||
and the fee will be taxed like the products in the order. If the products have multiple tax rates, multiple fees
|
||||
will be generated with weights adjusted to the net price of the products. Note that this will be calculated once
|
||||
during order generation and is not respected automatically when the order changes later.)
|
||||
and the fee will be taxed like the products in the order *unless* the total amount of the positions is zero.
|
||||
If the products have multiple tax rates, multiple fees will be generated with weights adjusted to the net price
|
||||
of the products. Note that this will be calculated once during order generation and is not respected automatically
|
||||
when the order changes later.)
|
||||
|
||||
* ``force`` (optional). If set to ``true``, quotas will be ignored.
|
||||
* ``send_email`` (optional). If set to ``true``, the same emails will be sent as for a regular order, regardless of
|
||||
|
||||
@@ -81,7 +81,8 @@ Endpoints
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID.
|
||||
:query integer subevent__in: Only return quotas of sub-events with one the given IDs (comma-separated).
|
||||
:query integer subevent__in: Only return quotas of sub-events with one of the given IDs (comma-separated).
|
||||
:query integer items__in: Only return quotas that include a product with one of the given IDs (comma-separated).
|
||||
:query string with_availability: Set to ``true`` to get availability information. Can lead to increased answer times.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
.. spelling:word-list::
|
||||
|
||||
AGPL
|
||||
AGPLv3
|
||||
GPL
|
||||
LGPL
|
||||
Apache
|
||||
BSD
|
||||
MIT
|
||||
CLA
|
||||
django
|
||||
i18nfields
|
||||
hierarkey
|
||||
rami.io
|
||||
rami
|
||||
io
|
||||
GmbH
|
||||
|
||||
License FAQ
|
||||
===========
|
||||
|
||||
.. warning::
|
||||
|
||||
This FAQ tries to explain in simpler terms what the license of the pretix open source project does and does not
|
||||
allow. It is based on our interpretation of the license and is not legal advice. The contents of this page are not
|
||||
legally binding, only the original text of the license in the `license file`_ is legally binding.
|
||||
|
||||
How is pretix licensed?
|
||||
-----------------------
|
||||
|
||||
pretix follows the popular dual licensing model. It is available under the `GNU Affero General Public License 3`_ (AGPL)
|
||||
plus some additional terms, as well as under a proprietary license ("pretix Enterprise license") on request.
|
||||
|
||||
How can it be AGPL if there are additional terms?
|
||||
-------------------------------------------------
|
||||
|
||||
Even though it is fairly unknown, the AGPL's section 7 is titled "Additional Terms" and outlines specific conditions
|
||||
under which additional terms can be imposed on an AGPL-licensed work. In our case, we add three additional terms.
|
||||
|
||||
The first additional term for pretix is an additional **permission**. It allows you to do something that the AGPL would
|
||||
generally not allow. As it doesn't restrict your freedoms granted by AGPL, if you don't like it, you can ignore it, and
|
||||
if you distribute pretix further, you can remove it.
|
||||
|
||||
The second and third additional term for pretix are additional terms that restrict or specify other provisions of the
|
||||
license. AGPL specifically requires that these terms can only restrict or specify very specific things and we believe
|
||||
our additional terms are in compliance with that and are thus valid and may not be removed.
|
||||
|
||||
Why did you choose this license model?
|
||||
--------------------------------------
|
||||
|
||||
pretix was born in the open source community and we're deeply committed to building the best open source ticketing
|
||||
solution in the world. It is important to us that pretix is available with a comprehensive feature set under term that
|
||||
are compatible with the `Open Source Definition`_. This enables event organizers from all industries and regions
|
||||
to have access to a self-hosted, privacy-friendly and secure option to host their events.
|
||||
|
||||
However, developing and maintaining pretix is a lot of work. Between 2014 and 2021, we've received external
|
||||
contributions from more than 150 individuals. Not counting translations over 90 % of the development was
|
||||
done by staff engineers of rami.io GmbH, the company that started pretix. While we're very happy to receive many more
|
||||
contributions in the future, we also want to ensure that we continue to be able to pay people working on pretix
|
||||
full-time.
|
||||
|
||||
We believe our model creates a good balance between ensuring pretix is available freely as well as protecting our
|
||||
business interests. Unlike licenses chosen by other projects recently, such as the Server-Side Public License, our
|
||||
choice does not restrict using pretix for any possible use case, it just sets a few rules that you have to play by
|
||||
if you do.
|
||||
|
||||
What do I need to do if I use pretix unmodified?
|
||||
------------------------------------------------
|
||||
|
||||
If you use pretix without any modifications or plugins, you can use it for whatever you want, as long as you keep
|
||||
all copyright notices (including the link to pretix at the bottom of the site) intact.
|
||||
|
||||
You are also allowed to make copies of the unmodified source code and distribute them to others as long as you keep
|
||||
all copyright and license information intact.
|
||||
|
||||
If you install **plugins**, you must follow the same terms as when using a **modified** version (see below).
|
||||
|
||||
What do I need to do if I modify pretix?
|
||||
----------------------------------------
|
||||
|
||||
If you want to modify pretix, you have the right to do so. However, you need to follow the following rules:
|
||||
|
||||
* If you **run it for your own events** (events run by you or your company as well as companies from the same
|
||||
corporate groups) our additional permission allows you to do so **without needing to share your source code
|
||||
modifications** as long as you keep the link to pretix at the bottom of the site intact.
|
||||
|
||||
* If you **run it for others**, for example as part of a Software-as-a-Service offering or a managed hosting service
|
||||
you **must** make the source code **including all your modifications and all installed plugins** available under the
|
||||
same license as pretix to every visitor of your site. You need to do so in a prominent place such as a link at the bottom of the
|
||||
site. You also **must** keep the existing link intact.
|
||||
You **may not** add additional restrictions on the result as a whole. You **may** add additional permissions, but
|
||||
only on the parts you added. You **must** make clear which changes you made and you must not give the impression that
|
||||
your modified version is an official version of pretix.
|
||||
|
||||
* If you **distribute** the modified version, for example as a source code or software package, you **must** license it
|
||||
under the AGPL license with the same additional terms. You **may not** add additional restrictions on the result as a
|
||||
whole. You **may** add additional permissions, but only on the parts you added. You **must** make clear which changes
|
||||
you made and you must not give the impression that your modified version is an official version of pretix.
|
||||
|
||||
Does the AGPL copyleft mechanism extend to plugins?
|
||||
---------------------------------------------------
|
||||
|
||||
Yes. pretix plugins are tightly integrated with pretix, so when running pretix together with a plugin in the same
|
||||
environment they form a `combined work`_ and the copyleft mechanism of AGPL applies.
|
||||
|
||||
Can I create proprietary or secret plugins?
|
||||
-------------------------------------------
|
||||
|
||||
Yes, you can create a proprietary or secret plugin, but it may only ever be **used** in an environment that is covered
|
||||
by the additional permission from our license. As soon as the plugin is installed in an installation that is not covered
|
||||
by our additional permission (e.g. when it is used in a SaaS environment) or covered by an active pretix Enterprise
|
||||
license it **must** be released to the visitors of the site under the same license as pretix (like a modified version
|
||||
of pretix).
|
||||
|
||||
What licenses can plugins use?
|
||||
------------------------------
|
||||
|
||||
Technically, you can distribute a plugin under any free or proprietary license as long as it is distributed separately.
|
||||
However, once it is either **distributed together with pretix or used in an environment not covered by our
|
||||
additional permission** or an active pretix Enterprise license, you **must** release it to all recipients of the
|
||||
distribution or all visitors of your site under the same license as pretix (like a modified version of pretix).
|
||||
|
||||
If you release a plugin publicly, it is therefore most practical to use a license that is `compatible to AGPL`_.
|
||||
This includes most open source licenses such as AGPL, GPL, Apache, 3-clause BSD or MIT.
|
||||
|
||||
Note however that when you license a plugin with pure AGPL, it will be incompatible with our additional permission.
|
||||
Therefore, if you want to use an AGPL-licensed plugin, you'll need to publish the source code of **all** your plugins
|
||||
under AGPL terms **even if you only use it for your own events**. A plugin would add its `own additional permission`_
|
||||
to its license to allow combining it with pretix for this use case.
|
||||
|
||||
To make things less complicated, if you want to distribute a plugin freely, we therefore recommend distributing the
|
||||
plugin under **Apache License 2.0**, like we do for most plugins we distribute as open source.
|
||||
|
||||
What do I need to do if I want to contribute my changes back?
|
||||
-------------------------------------------------------------
|
||||
|
||||
In order to retain the possibility for us to offer pretix in a dual licensing model, we unfortunately need you to sign
|
||||
a Contributor License Agreement (CLA) that gives us permission to use your contribution in all present and future
|
||||
distributions of pretix. We know the bureaucracy sucks. Sorry.
|
||||
|
||||
What if I want to re-use a minor part of pretix in my project?
|
||||
--------------------------------------------------------------
|
||||
|
||||
This is the main part we dislike about AGPL: If you see a specific thing in pretix that you'd like to use in another
|
||||
project, you'll need to distribute your other project under AGPL terms as well which is often not practical.
|
||||
|
||||
In this case, feel free to get in touch with us! We're happy to grant you special permission or pull the component
|
||||
out into a separately, permissively licensed repository. We already did that with `django-hierarkey`_ and
|
||||
`django-i18nfield`_ which have previously been parts of pretix.
|
||||
|
||||
What can I use the name "pretix" for?
|
||||
-------------------------------------
|
||||
|
||||
The name pretix is a registered trademark by rami.io GmbH.
|
||||
|
||||
* You **may** use it to **indicate copyright**, such as in the "powered by pretix" or "based on pretix" line, or when
|
||||
indicating that a distribution is based on pretix.
|
||||
|
||||
* You **may** use it to **indicate compatibility**, for example you are allowed to name your plugin "<name> for pretix"
|
||||
or you may state that an external service is compatible with pretix.
|
||||
|
||||
* You **may not** give the impression that your modified version, plugin or compatible service is official or authorized
|
||||
by rami.io GmbH or pretix unless we specifically allowed you to do so.
|
||||
|
||||
* You **may not** use it to name your modified version of pretix. End-users must be able to easily identify whether
|
||||
a version of pretix is distributed by us.
|
||||
|
||||
* You **may not** use any variations of the name, such as "MyPretix".
|
||||
|
||||
.. _license file: https://github.com/pretix/pretix/blob/master/LICENSE
|
||||
.. _GNU Affero General Public License 3: https://www.gnu.org/licenses/agpl-3.0.en.html
|
||||
.. _compatible to AGPL: https://www.gnu.org/licenses/license-list.en.html#GPLCompatibleLicenses
|
||||
.. _Open Source Definition: https://opensource.org/osd
|
||||
.. _combined work: https://www.gnu.org/licenses/gpl-faq.html#GPLPlugins
|
||||
.. _own additional permission: https://www.gnu.org/licenses/gpl-faq.html#GPLIncompatibleLibs
|
||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||
.. _django-i18nfield: https://github.com/raphaelm/django-i18nfield
|
||||
@@ -46,7 +46,7 @@ dependencies = [
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.10.*",
|
||||
"django-libsass==0.9",
|
||||
"django-localflavor==4.0",
|
||||
"django-localflavor==5.0",
|
||||
"django-markup",
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.6.*",
|
||||
|
||||
@@ -1600,7 +1600,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
self.context['event'].currency)
|
||||
is_split_taxes = fee_data.pop('_split_taxes_like_products', False)
|
||||
|
||||
if is_split_taxes:
|
||||
if is_split_taxes and order.total:
|
||||
d = defaultdict(lambda: Decimal('0.00'))
|
||||
trz = TaxRule.zero()
|
||||
for p in pos_map.values():
|
||||
|
||||
@@ -485,8 +485,17 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
with scopes_disabled():
|
||||
class QuotaFilter(FilterSet):
|
||||
items__in = NumberInFilter(
|
||||
field_name='items__id',
|
||||
lookup_expr='in',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = {
|
||||
@@ -508,7 +517,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
return self.request.event.quotas.all()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
queryset = self.filter_queryset(self.get_queryset()).distinct()
|
||||
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
|
||||
@@ -308,7 +308,10 @@ class WrappedPhonePrefixSelect(Select):
|
||||
self.initial = "+%d" % prefix
|
||||
break
|
||||
choices += get_phone_prefixes_sorted_and_localized()
|
||||
super().__init__(choices=choices, attrs={'aria-label': pgettext_lazy('phonenumber', 'International area code')})
|
||||
super().__init__(choices=choices, attrs={
|
||||
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
||||
'autocomplete': 'tel-country-code',
|
||||
})
|
||||
|
||||
def render(self, name, value, *args, **kwargs):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
@@ -331,11 +334,11 @@ class WrappedPhonePrefixSelect(Select):
|
||||
class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
|
||||
def __init__(self, attrs=None, initial=None):
|
||||
attrs = {
|
||||
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)')
|
||||
}
|
||||
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs=attrs))
|
||||
super(PhoneNumberPrefixWidget, self).__init__(widgets, attrs)
|
||||
widgets = (WrappedPhonePrefixSelect(initial), forms.TextInput(attrs={
|
||||
'aria-label': pgettext_lazy('phonenumber', 'Phone number (without international area code)'),
|
||||
'autocomplete': 'tel-national',
|
||||
}))
|
||||
super(PhoneNumberPrefixWidget, self).__init__(widgets)
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
output = super().render(name, value, attrs, renderer)
|
||||
@@ -992,6 +995,13 @@ class BaseQuestionsForm(forms.Form):
|
||||
value.initial = data.get('question_form_data', {}).get(key)
|
||||
|
||||
for k, v in self.fields.items():
|
||||
if isinstance(v.widget, forms.MultiWidget):
|
||||
for w in v.widget.widgets:
|
||||
autocomplete = w.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
w.attrs['autocomplete'] = 'off'
|
||||
else:
|
||||
w.attrs['autocomplete'] = 'section-{} '.format(self.prefix) + autocomplete
|
||||
if v.widget.attrs.get('autocomplete') or k == 'attendee_name_parts':
|
||||
autocomplete = v.widget.attrs.get('autocomplete', '')
|
||||
if autocomplete.strip() == "off":
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
|
||||
from pretix.base.models import (
|
||||
Discount, Item, ItemCategory, Order, Question, Quota, SubEvent, TaxRule,
|
||||
Voucher,
|
||||
Voucher, WaitingListEntry,
|
||||
)
|
||||
|
||||
from .logentrytype_registry import ( # noqa
|
||||
@@ -145,3 +145,15 @@ class TaxRuleLogEntryType(EventLogEntryType):
|
||||
object_link_viewname = 'control:event.settings.tax.edit'
|
||||
object_link_argname = 'rule'
|
||||
content_type = TaxRule
|
||||
|
||||
|
||||
class WaitingListEntryLogEntryType(EventLogEntryType):
|
||||
object_link_wrapper = _('{val}')
|
||||
object_link_viewname = 'control:event.orders.waitinglist'
|
||||
content_type = WaitingListEntry
|
||||
|
||||
def get_object_link_info(self, logentry) -> Optional[dict]:
|
||||
info = super().get_object_link_info(logentry)
|
||||
if info and 'href' in info:
|
||||
info['href'] += '?status=a&entry=' + str(logentry.content_object.pk)
|
||||
return info
|
||||
|
||||
@@ -1084,6 +1084,7 @@ class Event(EventMixin, LoggedModel):
|
||||
s.product = item_map[s.product_id]
|
||||
s.save(force_insert=True)
|
||||
|
||||
valid_sales_channel_identifers = set(self.organizer.sales_channels.values_list("identifier", flat=True))
|
||||
skip_settings = (
|
||||
'ticket_secrets_pretix_sig1_pubkey',
|
||||
'ticket_secrets_pretix_sig1_privkey',
|
||||
@@ -1119,6 +1120,11 @@ class Event(EventMixin, LoggedModel):
|
||||
settings_to_save.append(s)
|
||||
except ValueError:
|
||||
pass
|
||||
elif s.key.startswith('payment_') and s.key.endswith('__restrict_to_sales_channels'):
|
||||
data = other.settings._unserialize(s.value, as_type=list)
|
||||
data = [ident for ident in data if ident in valid_sales_channel_identifers]
|
||||
s.value = other.settings._serialize(data)
|
||||
settings_to_save.append(s)
|
||||
else:
|
||||
settings_to_save.append(s)
|
||||
other.settings._objects.bulk_create(settings_to_save)
|
||||
|
||||
@@ -218,7 +218,6 @@ class WaitingListEntry(LoggedModel):
|
||||
'waitinglistentry': self.pk,
|
||||
'subevent': self.subevent.pk if self.subevent else None,
|
||||
}, user=user, auth=auth)
|
||||
self.log_action('pretix.event.orders.waitinglist.voucher_assigned', user=user, auth=auth)
|
||||
self.voucher = v
|
||||
self.save()
|
||||
|
||||
@@ -234,6 +233,7 @@ class WaitingListEntry(LoggedModel):
|
||||
),
|
||||
user=user,
|
||||
auth=auth,
|
||||
log_entry_type='pretix.event.orders.waitinglist.voucher_assigned',
|
||||
)
|
||||
|
||||
def send_mail(self, subject: Union[str, LazyI18nString], template: Union[str, LazyI18nString],
|
||||
|
||||
@@ -50,7 +50,7 @@ from pretix.base.logentrytypes import (
|
||||
DiscountLogEntryType, EventLogEntryType, ItemCategoryLogEntryType,
|
||||
ItemLogEntryType, LogEntryType, OrderLogEntryType, QuestionLogEntryType,
|
||||
QuotaLogEntryType, TaxRuleLogEntryType, VoucherLogEntryType,
|
||||
log_entry_types,
|
||||
WaitingListEntryLogEntryType, log_entry_types,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Checkin, CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
@@ -697,11 +697,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
|
||||
'the last request was less than 24 hours ago.'),
|
||||
'pretix.organizer.deleted': _('The organizer "{name}" has been deleted.'),
|
||||
'pretix.waitinglist.voucher': _('A voucher has been sent to a person on the waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
|
||||
'pretix.event.order.waitinglist.transferred': _('An entry has been transferred to another waiting list.'), # legacy
|
||||
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
|
||||
'pretix.team.created': _('The team has been created.'),
|
||||
'pretix.team.changed': _('The team settings have been changed.'),
|
||||
'pretix.team.deleted': _('The team has been deleted.'),
|
||||
@@ -903,3 +899,13 @@ class LegacyCheckinLogEntryType(OrderLogEntryType):
|
||||
datetime=dt_formatted,
|
||||
list=checkin_list
|
||||
)
|
||||
|
||||
|
||||
@log_entry_types.new_from_dict({
|
||||
'pretix.event.orders.waitinglist.voucher_assigned': _('A voucher has been sent to a person on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.deleted': _('An entry has been removed from the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.changed': _('An entry has been changed on the waiting list.'),
|
||||
'pretix.event.orders.waitinglist.added': _('An entry has been added to the waiting list.'),
|
||||
})
|
||||
class CoreWaitingListEntryLogEntryType(WaitingListEntryLogEntryType):
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
from pretix.base.logentrytypes import (
|
||||
EventLogEntryType, OrderLogEntryType, log_entry_types,
|
||||
EventLogEntryType, OrderLogEntryType, WaitingListEntryLogEntryType,
|
||||
log_entry_types,
|
||||
)
|
||||
from pretix.base.models import SubEvent
|
||||
from pretix.base.signals import (
|
||||
@@ -130,6 +131,11 @@ class SendmailPluginOrderLogEntryType(OrderLogEntryType):
|
||||
pass
|
||||
|
||||
|
||||
@log_entry_types.new('pretix.plugins.sendmail.waitinglist.email.sent', _('The person on the waiting list received a mass email.'))
|
||||
class SendmailPluginWaitingListLogEntryType(WaitingListEntryLogEntryType):
|
||||
pass
|
||||
|
||||
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.added', _('An email rule was created'))
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.changed', _('An email rule was updated'))
|
||||
@log_entry_types.new('pretix.plugins.sendmail.rule.order.email.sent', _('A scheduled email was sent to the order'))
|
||||
|
||||
@@ -201,4 +201,5 @@ def send_mails_to_waitinglist(event: Event, user: int, subject: dict, message: d
|
||||
),
|
||||
user=user,
|
||||
attach_cached_files=attachments,
|
||||
log_entry_type='pretix.plugins.sendmail.waitinglist.email.sent',
|
||||
)
|
||||
|
||||
@@ -342,6 +342,7 @@ class ResetPasswordForm(forms.Form):
|
||||
}
|
||||
email = forms.EmailField(
|
||||
label=_('Email'),
|
||||
widget=forms.EmailInput(attrs={'autocomplete': 'email'}),
|
||||
)
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
@@ -389,12 +390,12 @@ class ChangePasswordForm(forms.Form):
|
||||
)
|
||||
password_current = forms.CharField(
|
||||
label=_('Your current password'),
|
||||
widget=forms.PasswordInput,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
required=True
|
||||
)
|
||||
password = forms.CharField(
|
||||
label=_('New password'),
|
||||
widget=forms.PasswordInput,
|
||||
widget=forms.PasswordInput(attrs={'minlength': '8', 'autocomplete': 'new-password'}),
|
||||
max_length=4096,
|
||||
required=True
|
||||
)
|
||||
@@ -458,7 +459,7 @@ class ChangeInfoForm(forms.ModelForm):
|
||||
}
|
||||
password_current = forms.CharField(
|
||||
label=_('Your current password'),
|
||||
widget=forms.PasswordInput,
|
||||
widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}),
|
||||
help_text=_('Only required if you change your email address'),
|
||||
max_length=4096,
|
||||
required=False
|
||||
@@ -472,6 +473,8 @@ class ChangeInfoForm(forms.ModelForm):
|
||||
self.request = request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields['email'].widget.attrs['autocomplete'] = 'email'
|
||||
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=True,
|
||||
|
||||
@@ -57,6 +57,8 @@ class WaitingListForm(forms.ModelForm):
|
||||
|
||||
event = self.event
|
||||
|
||||
self.fields['email'].widget.attrs['autocomplete'] = 'email'
|
||||
|
||||
if event.settings.waiting_list_names_asked:
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
<div class="panel-body questions-form">
|
||||
{% if form.position.seat %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Seat" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="5">{% trans "Seat" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
{% include "icons/seat.svg" with cls="svg-icon" %}
|
||||
{{ form.position.seat }}
|
||||
@@ -30,9 +30,9 @@
|
||||
{% endif %}
|
||||
{% if form.position.addons.all %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Selected add-ons" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="5">{% trans "Selected add-ons" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
{% for a in form.position.addons.all %}
|
||||
@@ -44,13 +44,13 @@
|
||||
{% endif %}
|
||||
{% if form.position.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="5">{% trans "Date" context "subevent" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
<p class="addon-list">
|
||||
{{ form.position.subevent.name }} · {{ form.position.subevent.get_date_range_display_with_times_as_html }}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -95,9 +95,9 @@
|
||||
{% endif %}
|
||||
{% if pos.seat %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Seat" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="4">{% trans "Seat" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
{% include "icons/seat.svg" with cls="svg-icon" %}
|
||||
{{ pos.seat }}
|
||||
@@ -106,9 +106,9 @@
|
||||
{% endif %}
|
||||
{% if pos.addons_without_bundled %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Selected add-ons" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="4">{% trans "Selected add-ons" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
{% regroup pos.addons_without_bundled by item_and_variation as addons_by_itemvar %}
|
||||
@@ -121,13 +121,13 @@
|
||||
{% endif %}
|
||||
{% if pos.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="4">{% trans "Date" context "subevent" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
<p class="addon-list">
|
||||
{{ pos.subevent.name }} · {{ pos.subevent.get_date_range_display_with_times_as_html }}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -96,8 +96,8 @@
|
||||
{% if not event.settings.show_variations_expanded %}
|
||||
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
|
||||
data-label-alt="{% trans "Hide variants" %}"
|
||||
aria-expanded="false"
|
||||
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{item}}{% endblocktrans %}">
|
||||
aria-expanded="false" aria-controls="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations"
|
||||
aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-legend">
|
||||
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
|
||||
<span>{% trans "Show variants" %}</span>
|
||||
</button>
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}" id="cp-{{ form.pos.pk }}-item-{{ item.pk }}-variations">
|
||||
{% for var in item.available_variations %}
|
||||
<article aria-labelledby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="cp-{{ form.pos.pk }}-item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row-fluid product-row variation"
|
||||
{% if not item.free_price %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% load icon %}
|
||||
{% load eventurl %}
|
||||
{% load daterange %}
|
||||
{% load safelink %}
|
||||
@@ -506,10 +507,23 @@
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-default" type="submit" id="cart-extend-button" aria-describedby="cart-deadline">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> {% trans "Renew reservation" %}
|
||||
{% icon "refresh" %} {% trans "Renew reservation" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<dialog role="alertdialog" id="cart-extend-confirmation-dialog" class="inline-dialog" aria-labelledby="cart-deadline">
|
||||
<form method="dialog">
|
||||
<p>
|
||||
<button class="btn btn-success" autofocus value="OK">
|
||||
<span role="img" aria-label="{% trans "OK" %}.">
|
||||
{% icon "check" %}
|
||||
</span>
|
||||
{% trans "Reservation renewed" %}
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
</dialog>
|
||||
{% else %}
|
||||
<p class="sr-only" id="cart-description">{% trans "Overview of your ordered products." %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
<div class="form-order-change-main">
|
||||
{% if position.subevent %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Date" context "subevent" %}
|
||||
</label>
|
||||
<div class="col-md-3 control-label">
|
||||
<strong role="heading" aria-level="4">{% trans "Date" context "subevent" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-9 form-control-text">
|
||||
<ul class="addon-list">
|
||||
<p class="addon-list">
|
||||
{{ position.subevent.name }} · {{ position.subevent.get_date_range_display_with_times_as_html }}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
{% endif %}
|
||||
<button type="button" data-toggle="variations" class="btn btn-default btn-block js-only"
|
||||
data-label-alt="{% trans "Hide variants" %}"
|
||||
aria-expanded="false"
|
||||
aria-label="{% blocktrans trimmed with item=item.name count=item.available_variations|length %}Show {{count}} variants of {{ item }}{% endblocktrans %}">
|
||||
aria-expanded="false" aria-controls="{{ form_prefix }}item-{{ item.pk }}-variations"
|
||||
aria-describedby="{{ form_prefix }}item-{{ item.pk }}-legend">
|
||||
<i class="fa fa-angle-down collapse-indicator" aria-hidden="true"></i>
|
||||
<span>{% trans "Show variants" %}</span>
|
||||
</button>
|
||||
@@ -110,7 +110,7 @@
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}">
|
||||
<div class="variations {% if not event.settings.show_variations_expanded %}variations-collapsed{% endif %}" id="{{ form_prefix }}item-{{ item.pk }}-variations">
|
||||
{% for var in item.available_variations %}
|
||||
<article aria-labelledby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-legend"{% if var.description %} aria-describedby="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}-description"{% endif %} class="row product-row variation" id="{{ form_prefix }}item-{{ item.pk }}-{{ var.pk }}"
|
||||
{% if not item.free_price %}
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
<div class="input-group{% if "voucher_invalid" in request.GET %} has-error{% endif %}">
|
||||
<span class="input-group-addon"><i class="fa fa-ticket fa-fw" aria-hidden="true"></i></span>
|
||||
<input type="text" class="form-control{% if "voucher_invalid" in request.GET %} has-error{% endif %}" name="voucher" id="voucher"
|
||||
{% if "voucher_invalid" in request.GET %} aria-describedby="error-message"{% endif %}
|
||||
placeholder="{% trans "Voucher code" %}" required="required">
|
||||
{% if "voucher_invalid" in request.GET %} aria-describedby="error-message"{% endif %}
|
||||
autocomplete="off"
|
||||
placeholder="{% trans "Voucher code" %}" required="required">
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="subevent" value="{{ subevent.id|default_if_none:"" }}" />
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="help-block">
|
||||
<p class="help-block" id="add-to-list-description">
|
||||
{% blocktrans trimmed %}
|
||||
You will <strong>not</strong> receive a confirmation email after you have been added to the waiting list. We will only contact you once a spot opens up.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<button type="submit" class="btn btn-primary" aria-describedby="add-to-list-description">
|
||||
{% trans "Add me to the list" %}
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
<p class="modal-card-confirm"><button class="btn btn-lg btn-primary">{% trans "Renew reservation" %}</button></p>
|
||||
{% enddialog %}
|
||||
|
||||
{% dialog "dialog-cart-extended" "" "" icon="clock-o" alert=true %}
|
||||
<p class="modal-card-confirm"><button class="btn btn-lg btn-primary">{% trans "OK" %}</button></p>
|
||||
{% enddialog %}
|
||||
|
||||
<dialog id="lightbox-dialog" class="modal-card" role="alertdialog" aria-labelledby="lightbox-label">
|
||||
<form method="dialog" class="modal-card-inner form-horizontal">
|
||||
<div class="modal-card-content">
|
||||
|
||||
@@ -117,7 +117,7 @@ setup_collapsible_details = function (el) {
|
||||
el.find("article button[data-toggle=variations]").click(function (e) {
|
||||
var $button = $(this);
|
||||
var $details = $button.closest("article");
|
||||
var $detailsNotSummary = $(".variations", $details);
|
||||
var $detailsNotSummary = $button.attr("aria-controls") ? $('#' + $button.attr("aria-controls")) : $(".variations", $details);
|
||||
var isOpen = !$detailsNotSummary.prop("hidden");
|
||||
if ($detailsNotSummary.is(':animated')) {
|
||||
e.preventDefault();
|
||||
@@ -125,7 +125,7 @@ setup_collapsible_details = function (el) {
|
||||
}
|
||||
|
||||
var altLabel = $button.attr("data-label-alt");
|
||||
$button.attr("data-label-alt", $button.text());
|
||||
$button.attr("data-label-alt", $button.text().trim());
|
||||
$button.find("span").text(altLabel);
|
||||
$button.attr("aria-expanded", !isOpen);
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
dialog.inline-dialog {
|
||||
position: static;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Modal dialogs using HTML5 dialog tags for accessibility */
|
||||
dialog.modal-card {
|
||||
border: none;
|
||||
|
||||
@@ -7,17 +7,16 @@ $(function () {
|
||||
|
||||
$(".chart").css("height", "250px");
|
||||
var data_type = $("#question_chart").attr("data-type"),
|
||||
data = JSON.parse($("#question-chart-data").html()),
|
||||
data = JSON.parse($("#question-chart-data").text() || "[]"),
|
||||
others_sum = 0,
|
||||
max_num = 8;
|
||||
|
||||
for (var i in data) {
|
||||
data[i].value = data[i].count;
|
||||
data[i].label = data[i].answer;
|
||||
if (data[i].label.length > 20) {
|
||||
data[i].label = data[i].label.substring(0, 20) + '…';
|
||||
data = data[0]?.map(function (d) {
|
||||
return {
|
||||
'value': d.count,
|
||||
'label': d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
|
||||
}
|
||||
}
|
||||
}) || [];
|
||||
|
||||
if (data_type == 'N') {
|
||||
// Sort
|
||||
@@ -36,7 +35,7 @@ $(function () {
|
||||
// Limit shown options
|
||||
if (data.length > max_num) {
|
||||
for (var i = max_num; i < data.length; i++) {
|
||||
others_sum += data[i].count;
|
||||
others_sum += data[i].value;
|
||||
}
|
||||
data = data.slice(0, max_num);
|
||||
data.push({'value': others_sum, 'label': gettext('Others')});
|
||||
@@ -78,7 +77,7 @@ $(function () {
|
||||
data: data,
|
||||
resize: true,
|
||||
xkey: 'label',
|
||||
ykeys: ['count'],
|
||||
ykeys: ['value'],
|
||||
labels: [gettext('Count')]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ var cart = {
|
||||
_deadline_call: 0,
|
||||
_time_offset: 0,
|
||||
_prev_diff_minutes: 0,
|
||||
_renewed_message: "",
|
||||
|
||||
_get_now: function () {
|
||||
return moment().add(cart._time_offset, 'ms');
|
||||
@@ -59,7 +58,6 @@ var cart = {
|
||||
$("#cart-deadline").text(gettext("Your cart is about to expire."))
|
||||
} else {
|
||||
$("#cart-deadline").text(
|
||||
cart._renewed_message + " " +
|
||||
ngettext(
|
||||
"The items in your cart are reserved for you for one minute.",
|
||||
"The items in your cart are reserved for you for {num} minutes.",
|
||||
@@ -74,7 +72,6 @@ var cart = {
|
||||
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
|
||||
);
|
||||
|
||||
cart._renewed_message = "";
|
||||
cart._deadline_timeout = window.setTimeout(cart.draw_deadline, 500);
|
||||
}
|
||||
var already_expired = diff_total_seconds <= 0;
|
||||
@@ -112,7 +109,6 @@ var cart = {
|
||||
}
|
||||
cart._deadline_timeout = null;
|
||||
cart._max_extend = moment(max_extend);
|
||||
cart._renewed_message = renewed_message || "";
|
||||
cart.draw_deadline();
|
||||
}
|
||||
};
|
||||
@@ -122,22 +118,41 @@ $(function () {
|
||||
|
||||
if ($("#cart-deadline").length) {
|
||||
cart.init();
|
||||
$("#cart-extend-confirmation-button").hide().on("blur", function() {
|
||||
$(this).hide();
|
||||
});
|
||||
}
|
||||
|
||||
$("#cart-extend-form").on("pretix:async-task-success", function(e, data) {
|
||||
if (data.success) {
|
||||
cart.set_deadline(data.expiry, data.max_expiry_extend, data.message);
|
||||
var cart_panel_heading = $(this).closest(".panel").find(".panel-heading").get(0);
|
||||
if (cart_panel_heading) {
|
||||
cart_panel_heading.focus();
|
||||
}
|
||||
cart.set_deadline(data.expiry, data.max_expiry_extend);
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
// renew-button in cart-panel is clicked, show inline dialog
|
||||
$("#cart-extend-button").on("click", function() {
|
||||
$("#cart-extend-form").one("pretix:async-task-success", function(e, data) {
|
||||
if (data.success) {
|
||||
document.getElementById("cart-extend-confirmation-dialog").show();
|
||||
}
|
||||
});
|
||||
});
|
||||
$("#cart-extend-confirmation-dialog").on("keydown", function (e) {
|
||||
if(e.key === "Escape") {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
// renew-button in modal dialog is clicked, show modal dialog
|
||||
$("#dialog-cart-extend form").submit(function() {
|
||||
$("#cart-extend-form").submit();
|
||||
$("#cart-extend-form").one("pretix:async-task-success", function(e, data) {
|
||||
if (data.success) {
|
||||
$("#dialog-cart-extended-title").text(data.message);
|
||||
$("#dialog-cart-extended-description").text($("#cart-deadline").text());
|
||||
document.getElementById("dialog-cart-extended").showModal();
|
||||
}
|
||||
}).submit();
|
||||
});
|
||||
|
||||
$(".toggle-container").each(function() {
|
||||
|
||||
@@ -7,4 +7,12 @@ var inIframe = function () {
|
||||
};
|
||||
if (inIframe()) {
|
||||
document.documentElement.classList.add('in-iframe');
|
||||
try {
|
||||
window.parent.postMessage({
|
||||
type: "pretix:widget:title",
|
||||
title: document.title,
|
||||
}, "*");
|
||||
} catch (e) {
|
||||
console.error("Could not post message to parent.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ Vue.component('availbox', {
|
||||
variation: Object
|
||||
},
|
||||
mounted: function() {
|
||||
if (this.$root.itemnum === 1 && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
this.$refs.quantity.value = 1;
|
||||
if (this.order_max === 1) {
|
||||
this.$refs.quantity.checked = true;
|
||||
@@ -823,7 +823,7 @@ var shared_loading_fragment = (
|
||||
);
|
||||
|
||||
var shared_iframe_fragment = (
|
||||
'<dialog :class="frameClasses" role="alertdialog" aria-label="'+strings.checkout+'" @close="close" @cancel="cancel">'
|
||||
'<dialog :class="frameClasses" aria-label="'+strings.checkout+'" @close="close" @cancel="cancel">'
|
||||
+ '<div class="pretix-widget-frame-loading" v-show="$root.frame_loading">'
|
||||
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
|
||||
+ '<p :class="cancelBlockedClasses"><strong>'+strings.cancel_blocked+'</strong></p>'
|
||||
@@ -903,6 +903,14 @@ Vue.component('pretix-overlay', {
|
||||
}
|
||||
}
|
||||
},
|
||||
'$root.frame_shown': function (newValue) {
|
||||
if (newValue) {
|
||||
var btn = this.$el?.querySelector('.pretix-widget-frame-close button');
|
||||
this.$nextTick(function() {
|
||||
btn.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
frameClasses: function () {
|
||||
@@ -931,7 +939,18 @@ Vue.component('pretix-overlay', {
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('message', this.onMessage, false);
|
||||
},
|
||||
unmounted () {
|
||||
window.removeEventListener('message', this.onMessage, false);
|
||||
},
|
||||
methods: {
|
||||
onMessage: function(e) {
|
||||
if (e.data.type && e.data.type == "pretix:widget:title") {
|
||||
this.$el.querySelector("iframe").title = e.data.title;
|
||||
}
|
||||
},
|
||||
lightboxClose: function () {
|
||||
this.$root.lightbox = null;
|
||||
},
|
||||
@@ -1563,14 +1582,14 @@ Vue.component('pretix-widget-event-calendar', {
|
||||
+ '<a class="pretix-widget-event-calendar-previous-month" href="#" @click.prevent.stop="prevmonth">« '
|
||||
+ strings['previous_month']
|
||||
+ '</a> '
|
||||
+ '<strong>{{ monthname }}</strong> '
|
||||
+ '<strong :id="aria_labelledby">{{ monthname }}</strong> '
|
||||
+ '<a class="pretix-widget-event-calendar-next-month" href="#" @click.prevent.stop="nextmonth">'
|
||||
+ strings['next_month']
|
||||
+ ' »</a>'
|
||||
+ '</div>'
|
||||
|
||||
// Calendar
|
||||
+ '<table class="pretix-widget-event-calendar-table" :id="id" tabindex="0" v-bind:aria-label="monthname">'
|
||||
+ '<table class="pretix-widget-event-calendar-table" :id="id" tabindex="0" v-bind:aria-labelledby="aria_labelledby">'
|
||||
+ '<thead>'
|
||||
+ '<tr>'
|
||||
+ '<th aria-label="' + strings['days']['MONDAY'] + '">' + strings['days']['MO'] + '</th>'
|
||||
@@ -1597,6 +1616,9 @@ Vue.component('pretix-widget-event-calendar', {
|
||||
id: function () {
|
||||
return this.$root.html_id + "-event-calendar-table";
|
||||
},
|
||||
aria_labelledby: function () {
|
||||
return this.$root.html_id + "-event-calendar-table-label";
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
back_to_list: function () {
|
||||
@@ -2182,11 +2204,22 @@ var create_overlay = function (app) {
|
||||
// show loading spinner only when previously no frame_src was set
|
||||
if (newValue && !oldValue) {
|
||||
this.frame_loading = true;
|
||||
this.$el?.querySelector('dialog.pretix-widget-frame-holder').showModal();
|
||||
}
|
||||
// to close and unload the iframe, frame_src can be empty -> make it valid HTML with about:blank
|
||||
this.$el.querySelector("iframe").src = newValue || "about:blank";
|
||||
},
|
||||
frame_loading: function (newValue) {
|
||||
var dialog = this.$el?.querySelector('dialog.pretix-widget-frame-holder');
|
||||
if (newValue) {
|
||||
if (!dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
} else {
|
||||
if (!this.frame_src && dialog.open) {// finished loading, but no iframe to display => close
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
app.$root.overlay = framechild;
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
line-height: normal;
|
||||
border: 1px solid $input-border;
|
||||
border-radius: $input-border-radius;
|
||||
height: $input-height-base;
|
||||
min-height: $input-height-base;
|
||||
padding: $padding-base-vertical $padding-base-horizontal;
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
|
||||
@@ -1901,10 +1901,11 @@ TEST_QUOTA_RES = {
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_list(token_client, organizer, event, quota, item, subevent):
|
||||
def test_quota_list(token_client, organizer, event, quota, item, item3, subevent):
|
||||
quota.items.add(item3)
|
||||
res = dict(TEST_QUOTA_RES)
|
||||
res["id"] = quota.pk
|
||||
res["items"] = [item.pk]
|
||||
res["items"] = [item.pk, item3.pk]
|
||||
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/quotas/'.format(organizer.slug, event.slug))
|
||||
assert resp.status_code == 200
|
||||
@@ -1922,6 +1923,13 @@ def test_quota_list(token_client, organizer, event, quota, item, subevent):
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?subevent={}'.format(organizer.slug, event.slug, se2.pk))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?items__in={},{},0'.format(organizer.slug, event.slug, item.pk, item3.pk))
|
||||
assert [res] == resp.data['results']
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/quotas/?items__in=0'.format(organizer.slug, event.slug))
|
||||
assert [] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_quota_detail(token_client, organizer, event, quota, item):
|
||||
|
||||
@@ -961,6 +961,42 @@ def test_order_create_fee_as_percentage(token_client, organizer, event, item, qu
|
||||
assert o.total == Decimal('25.30')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_as_percentage_with_zero(token_client, organizer, event, item, quota, question):
|
||||
with scopes_disabled():
|
||||
voucher = event.vouchers.create(price_mode="set", value=Decimal("0.00"))
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
res['fees'][0]['_treat_value_as_percentage'] = True
|
||||
res['fees'][0]['_split_taxes_like_products'] = True
|
||||
res['fees'][0]['value'] = '10.00'
|
||||
res['positions'][0]['item'] = item.pk
|
||||
res['positions'][0]['answers'][0]['question'] = question.pk
|
||||
res['positions'][0]['voucher'] = voucher.code
|
||||
del res['positions'][0]['price']
|
||||
|
||||
res['simulate'] = True
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["total"] == "0.00"
|
||||
|
||||
res['simulate'] = False
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/events/{}/orders/'.format(
|
||||
organizer.slug, event.slug
|
||||
), format='json', data=res
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
o = Order.objects.get(code=resp.data['code'])
|
||||
fee = o.fees.first()
|
||||
assert fee.value == Decimal('0.00')
|
||||
assert o.total == Decimal('0.00')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_create_fee_with_auto_tax(token_client, organizer, event, item, quota, question, taxrule):
|
||||
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
|
||||
|
||||
@@ -225,8 +225,11 @@ def test_full_clone_cross_organizer_differences():
|
||||
organizer2 = Organizer.objects.create(name='Dummy2', slug='dummy2')
|
||||
membership_type = organizer.membership_types.create(name="Membership")
|
||||
plan = SeatingPlan.objects.create(name="Plan", organizer=organizer, layout="{}")
|
||||
sc = organizer.sales_channels.get(identifier="web")
|
||||
sc2 = organizer2.sales_channels.get(identifier="web")
|
||||
sc1_a = organizer.sales_channels.get(identifier="web")
|
||||
sc1_b = organizer.sales_channels.create(identifier="b")
|
||||
sc1_c = organizer.sales_channels.create(identifier="c")
|
||||
sc2_a = organizer2.sales_channels.get(identifier="web")
|
||||
sc2_c = organizer2.sales_channels.create(identifier="c")
|
||||
|
||||
event = Event.objects.create(
|
||||
organizer=organizer, name='Dummy', slug='dummy',
|
||||
@@ -237,15 +240,20 @@ def test_full_clone_cross_organizer_differences():
|
||||
seating_plan=plan,
|
||||
all_sales_channels=False,
|
||||
)
|
||||
event.limit_sales_channels.add(sc)
|
||||
event.limit_sales_channels.add(sc1_a)
|
||||
event.limit_sales_channels.add(sc1_b)
|
||||
event.limit_sales_channels.add(sc1_c)
|
||||
|
||||
item1 = event.items.create(name="Ticket", default_price=23,
|
||||
grant_membership_type=membership_type,
|
||||
all_sales_channels=False)
|
||||
item1.limit_sales_channels.add(sc)
|
||||
item1.limit_sales_channels.add(sc1_a)
|
||||
item2 = event.items.create(name="T-shirt", default_price=15)
|
||||
item2.require_membership_types.add(membership_type)
|
||||
|
||||
event.settings.payment_giftcard__enabled = True
|
||||
event.settings.payment_giftcard__restrict_to_sales_channels = ['web', 'b', 'c']
|
||||
|
||||
copied_event = Event.objects.create(
|
||||
organizer=organizer2, name='Dummy2', slug='dummy2',
|
||||
date_from=datetime.datetime(2022, 4, 15, 9, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
@@ -257,11 +265,14 @@ def test_full_clone_cross_organizer_differences():
|
||||
assert organizer2.seating_plans.count() == 1
|
||||
assert organizer2.seating_plans.get().layout == plan.layout
|
||||
assert copied_event.seating_plan.organizer == organizer2
|
||||
assert copied_event.limit_sales_channels.get() == sc2
|
||||
assert set(copied_event.limit_sales_channels.all()) == {sc2_a, sc2_c}
|
||||
assert event.seating_plan.organizer == organizer
|
||||
|
||||
copied_item1 = copied_event.items.get(name=item1.name)
|
||||
copied_item2 = copied_event.items.get(name=item2.name)
|
||||
assert copied_item1.grant_membership_type is None
|
||||
assert copied_item2.require_membership_types.count() == 0
|
||||
assert copied_item1.limit_sales_channels.get() == sc2
|
||||
assert copied_item1.limit_sales_channels.get() == sc2_a
|
||||
|
||||
assert event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'b', 'c']
|
||||
assert copied_event.settings.get('payment_giftcard__restrict_to_sales_channels', as_type=list) == ['web', 'c']
|
||||
|
||||
Reference in New Issue
Block a user