Compare commits

..
9 changed files with 68 additions and 55 deletions
+1 -1
View File
@@ -203,7 +203,7 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.', 'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min' 'min'
), ),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), 'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'), 'addon_only': gettext_lazy('One of the products you selected can only be bought as an add-on to another product.'),
'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'), 'bundled_only': gettext_lazy('One of the products you selected can only be bought part of a bundle.'),
'seat_required': gettext_lazy('You need to select a specific seat.'), 'seat_required': gettext_lazy('You need to select a specific seat.'),
+1 -1
View File
@@ -197,7 +197,7 @@ error_messages = {
'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.', 'You need to select at least %(min)s add-ons from the category %(cat)s for the product %(base)s.',
'min' 'min'
), ),
'addon_no_multi': gettext_lazy('You can select every add-ons from the category %(cat)s for the product %(base)s at most once.'), 'addon_no_multi': gettext_lazy('You can select every add-on from the category %(cat)s for the product %(base)s at most once.'),
'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'), 'addon_already_checked_in': gettext_lazy('You cannot remove the position %(addon)s since it has already been checked in.'),
} }
-11
View File
@@ -31,7 +31,6 @@ from django import forms
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core.cache import cache from django.core.cache import cache
from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.templatetags.static import static from django.templatetags.static import static
@@ -56,7 +55,6 @@ from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.services.mail import SendMailException from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF
from pretix.helpers.urls import build_absolute_uri as build_global_uri from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.plugins.paypal2.client.core.environment import ( from pretix.plugins.paypal2.client.core.environment import (
@@ -588,9 +586,6 @@ class PaypalMethod(BasePaymentProvider):
}, },
}) })
response = self.client.execute(paymentreq) response = self.client.execute(paymentreq)
if payment:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=response.result.id)
except IOError as e: except IOError as e:
if "RESOURCE_NOT_FOUND" in str(e): if "RESOURCE_NOT_FOUND" in str(e):
messages.error(request, _('Your payment has failed due to a known issue within PayPal. Please try ' messages.error(request, _('Your payment has failed due to a known issue within PayPal. Please try '
@@ -623,13 +618,7 @@ class PaypalMethod(BasePaymentProvider):
} }
return template.render(ctx) return template.render(ctx)
@transaction.atomic
def execute_payment(self, request: HttpRequest, payment: OrderPayment): def execute_payment(self, request: HttpRequest, payment: OrderPayment):
payment = OrderPayment.objects.select_for_update(of=OF_SELF).get(pk=payment.pk)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('payment is already confirmed; possible return-view/webhook race-condition')
return
try: try:
if request.session.get('payment_paypal_oid', '') == '': if request.session.get('payment_paypal_oid', '') == '':
raise PaymentException(_('We were unable to process your payment. See below for details on how to ' raise PaymentException(_('We were unable to process your payment. See below for details on how to '
+20 -26
View File
@@ -482,35 +482,29 @@ def webhook(request, *args, **kwargs):
amount=payment.amount - known_sum amount=payment.amount - known_sum
) )
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED, elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED): OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) \
if sale['status'] == 'COMPLETED': and sale['status'] == 'COMPLETED':
any_captures = False any_captures = False
all_captures_completed = True all_captures_completed = True
for purchaseunit in sale['purchase_units']: for purchaseunit in sale['purchase_units']:
for capture in purchaseunit['payments']['captures']: for capture in purchaseunit['payments']['captures']:
try:
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment,
reference=capture['id'])
except ReferencedPayPalObject.MultipleObjectsReturned:
pass
if capture['status'] not in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
all_captures_completed = False
else:
any_captures = True
if any_captures and all_captures_completed:
try: try:
payment.info = json.dumps(sale.dict()) ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment,
payment.save(update_fields=['info']) reference=capture['id'])
payment.confirm() except ReferencedPayPalObject.MultipleObjectsReturned:
except Quota.QuotaExceededException:
pass pass
elif sale['status'] == 'APPROVED':
request.session['payment_paypal_oid'] = payment.info_data['id'] if capture['status'] not in ('COMPLETED', 'REFUNDED', 'PARTIALLY_REFUNDED'):
all_captures_completed = False
else:
any_captures = True
if any_captures and all_captures_completed:
try: try:
payment.payment_provider.execute_payment(request, payment) payment.info = json.dumps(sale.dict())
except PaymentException as e: payment.save(update_fields=['info'])
logger.exception('PayPal2 - Could not capture/execute_payment from Webhook: {}'.format(str(e))) payment.confirm()
except Quota.QuotaExceededException:
pass
return HttpResponse(status=200) return HttpResponse(status=200)
@@ -6,13 +6,13 @@
{% load eventsignal %} {% load eventsignal %}
{% load rich_text %} {% load rich_text %}
{% for c in form.categories %} {% for c in form.categories %}
<fieldset> <fieldset data-addon-max-count="{{ c.max_count }}"{% if c.multi_allowed %} data-addon-multi-allowed{% endif %}>
<legend>{{ c.category.name }}</legend> <legend>{{ c.category.name }}</legend>
{% if c.category.description %} {% if c.category.description %}
{{ c.category.description|rich_text }} {{ c.category.description|rich_text }}
{% endif %} {% endif %}
{% if c.min_count == c.max_count %} {% if c.min_count == c.max_count %}
<p> <p class="addon-count-desc">
{% blocktrans trimmed count min_count=c.min_count %} {% blocktrans trimmed count min_count=c.min_count %}
You need to choose exactly one option from this category. You need to choose exactly one option from this category.
{% plural %} {% plural %}
@@ -21,7 +21,7 @@
</p> </p>
{% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %} {% elif c.min_count == 0 and c.max_count >= c.items|length and not c.multi_allowed %}
{% elif c.min_count == 0 %} {% elif c.min_count == 0 %}
<p> <p class="addon-count-desc">
{% blocktrans trimmed count max_count=c.max_count %} {% blocktrans trimmed count max_count=c.max_count %}
You can choose {{ max_count }} option from this category. You can choose {{ max_count }} option from this category.
{% plural %} {% plural %}
@@ -29,7 +29,7 @@
{% endblocktrans %} {% endblocktrans %}
</p> </p>
{% else %} {% else %}
<p> <p class="addon-count-desc">
{% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %} {% blocktrans trimmed with min_count=c.min_count max_count=c.max_count %}
You can choose between {{ min_count }} and {{ max_count }} options from You can choose between {{ min_count }} and {{ max_count }} options from
this category. this category.
@@ -193,7 +193,6 @@
{% endif %} {% endif %}
id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" id="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}" name="cp_{{ form.pos.pk }}_variation_{{ item.id }}_{{ var.id }}"
data-exclusive-prefix="cp_{{ form.pos.pk }}_variation_{{ item.id }}_"
aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}"> aria-label="{% blocktrans with item=item.name var=var %}Add {{ item }}, {{ var }} to cart{% endblocktrans %}">
<i class="fa fa-shopping-cart" aria-hidden="true"></i> <i class="fa fa-shopping-cart" aria-hidden="true"></i>
{% trans "Select" context "checkbox" %} {% trans "Select" context "checkbox" %}
@@ -239,7 +239,7 @@
<ul> <ul>
{% for i in invoices %} {% for i in invoices %}
<li> <li>
<a href="{% eventurl event "presale:event.invoice.download" invoice=i.pk secret=order.secret order=order.code %}"> <a href="{% eventurl event "presale:event.invoice.download" invoice=i.pk secret=order.secret order=order.code %}" target="_blank">
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %} {% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }}) {{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
</li> </li>
@@ -625,6 +625,7 @@ var form_handlers = function (el) {
el.find('[data-model-select2=event]').each(function () { el.find('[data-model-select2=event]').each(function () {
var $s = $(this); var $s = $(this);
$s.select2({ $s.select2({
closeOnSelect: !this.hasAttribute('multiple'),
theme: "bootstrap", theme: "bootstrap",
delay: 100, delay: 100,
allowClear: !$s.prop("required"), allowClear: !$s.prop("required"),
@@ -904,8 +904,8 @@ details {
} }
} }
.select2-container [aria-multiselectable] .select2-results__option span strike::before, .select2-container [aria-multiselectable] .select2-results__option > span > strike:first-child::before,
.select2-container [aria-multiselectable] .select2-results__option span span::before { .select2-container [aria-multiselectable] .select2-results__option > span > span:first-child::before {
content: ""; content: "";
font-family: FontAwesome; font-family: FontAwesome;
display: inline-block; display: inline-block;
@@ -913,8 +913,8 @@ details {
width: 1.28571em; width: 1.28571em;
text-align: center; text-align: center;
} }
.select2-container [aria-multiselectable] .select2-results__option[aria-selected=true] span strike::before, .select2-container [aria-multiselectable] .select2-results__option[aria-selected=true] > span > strike:first-child::before,
.select2-container [aria-multiselectable] .select2-results__option[aria-selected=true] span span::before { .select2-container [aria-multiselectable] .select2-results__option[aria-selected=true] > span > span:first-child::before {
content: "" content: ""
} }
+36 -6
View File
@@ -123,7 +123,7 @@ var form_handlers = function (el) {
var controls = document.getElementById(this.getAttribute("data-controls")); var controls = document.getElementById(this.getAttribute("data-controls"));
var currentValue = parseFloat(controls.value); var currentValue = parseFloat(controls.value);
controls.value = Math.max(controls.min, Math.min(controls.max || Number.MAX_SAFE_INTEGER, (currentValue || 0) + step)); controls.value = Math.max(controls.min, Math.min(controls.max || Number.MAX_SAFE_INTEGER, (currentValue || 0) + step));
controls.dispatchEvent(new Event("change")); controls.dispatchEvent(new Event("change", { bubbles: true }));
}); });
el.find(".btn-checkbox input").on("change", function (e) { el.find(".btn-checkbox input").on("change", function (e) {
$(this).closest(".btn-checkbox") $(this).closest(".btn-checkbox")
@@ -149,11 +149,41 @@ var form_handlers = function (el) {
).find("canvas").attr("role", "img").attr("aria-label", this.getAttribute("data-desc")); ).find("canvas").attr("role", "img").attr("aria-label", this.getAttribute("data-desc"));
}); });
el.find("input[data-exclusive-prefix]").each(function () {
var $others = $("input[name^=" + $(this).attr("data-exclusive-prefix") + "]:not([name=" + $(this).attr("name") + "])"); el.find("fieldset[data-addon-max-count]").each(function() {
$(this).on('click change', function () { // usually addons are only allowed once one per item
if ($(this).prop('checked')) { var multipleAllowed = this.hasAttribute("data-addon-multi-allowed");
$others.prop('checked', false); var $inputs = $(".availability-box input", this);
var max = parseInt(this.getAttribute("data-addon-max-count"));
var desc = $(".addon-count-desc", this).text().trim();
this.addEventListener("change", function (e) {
var variations = e.target.closest(".variations");
if (variations && !multipleAllowed && e.target.checked) {
// uncheck all other checkboxes inside this variations
$(".availability-box input:checked", variations).not(e.target).prop("checked", false).trigger("change");
}
if (max === 1) {
if (e.target.checked) {
$inputs.filter(":checked").not(e.target).prop("checked", false).trigger("change");
}
return;
}
var total = $inputs.toArray().reduce(function(a, e) {
return a + (e.type == "checkbox" ? (e.checked ? parseInt(e.value) : 0) : parseInt(e.value) || 0);
}, 0);
if (total > max) {
if (e.target.type == "checkbox") {
e.target.checked = false;
} else {
e.target.value = e.target.value - (total - max);
}
$(e.target).trigger("change").closest(".availability-box").tooltip({
"title": desc,
}).tooltip('show');
e.preventDefault();
} else {
$(".availability-box", this).tooltip('destroy')
} }
}); });
}); });