mirror of
https://github.com/pretix/pretix.git
synced 2026-04-25 23:42:32 +00:00
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
"""
|
|
This module contains helper functions that are supposed to call out code paths missing calls to
|
|
``Order.create_transaction()`` by actively breaking them. Read the docstring of the ``Transaction`` class for a
|
|
detailed reasoning why this exists.
|
|
"""
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import threading
|
|
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
|
|
dirty_transactions = threading.local()
|
|
|
|
logger = logging.getLogger(__name__)
|
|
fail_loudly = os.getenv('PRETIX_DIRTY_TRANSACTIONS_QUIET', 'false' if settings.DEBUG else 'true') not in ('true', 'True', 'on', '1')
|
|
|
|
|
|
class DirtyTransactionsForOrderException(Exception):
|
|
pass
|
|
|
|
|
|
def _fail(message):
|
|
if fail_loudly:
|
|
raise DirtyTransactionsForOrderException(message)
|
|
else:
|
|
if settings.SENTRY_ENABLED:
|
|
import sentry_sdk
|
|
|
|
sentry_sdk.capture_message(message, "fatal")
|
|
|
|
logger.warning(message, stack_info=True)
|
|
|
|
|
|
def _check_for_dirty_orders():
|
|
if getattr(dirty_transactions, 'order_ids', None) is None:
|
|
dirty_transactions.order_ids = set()
|
|
try:
|
|
if dirty_transactions.order_ids and dirty_transactions.order_ids != {None}:
|
|
_fail(
|
|
f"In the transaction that just ended, you created or modified an Order, OrderPosition, or OrderFee "
|
|
f"object in a way that you should have called `order.create_transactions()` afterwards. The transaction "
|
|
f"still went through and your data can be fixed with the `create_order_transactions` management command "
|
|
f"but you should update your code to prevent this from happening. Affected order IDs: {dirty_transactions.order_ids}"
|
|
)
|
|
finally:
|
|
dirty_transactions.order_ids.clear()
|
|
|
|
|
|
def _transactions_mark_order_dirty(order_id, using=None):
|
|
if "PYTEST_CURRENT_TEST" in os.environ:
|
|
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
|
|
# or not.
|
|
for frame in inspect.stack():
|
|
if 'pretix/base/models/orders' in frame.filename:
|
|
continue
|
|
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
|
|
return
|
|
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
|
|
# This went through non-test code, let's consider it non-test
|
|
break
|
|
|
|
if order_id is None:
|
|
return
|
|
|
|
conn = transaction.get_connection(using)
|
|
if not conn.in_atomic_block:
|
|
_fail(
|
|
"You modified an Order, OrderPosition, or OrderFee object in a way that should create "
|
|
"a new Transaction object within the same database transaction, however you are not "
|
|
"doing it inside a database transaction!"
|
|
)
|
|
|
|
if getattr(dirty_transactions, 'order_ids', None) is None:
|
|
dirty_transactions.order_ids = set()
|
|
|
|
if _check_for_dirty_orders not in [func for (savepoint_id, func, *__) in conn.run_on_commit]:
|
|
transaction.on_commit(_check_for_dirty_orders, using)
|
|
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions
|
|
|
|
dirty_transactions.order_ids.add(order_id)
|
|
|
|
|
|
def _transactions_mark_order_clean(order_id):
|
|
if getattr(dirty_transactions, 'order_ids', None) is None:
|
|
dirty_transactions.order_ids = set()
|
|
try:
|
|
dirty_transactions.order_ids.remove(order_id)
|
|
except KeyError:
|
|
pass
|