Changeset - 587e6e20b284
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-04-29 01:08:45
chrisjrn@gmail.com
Adds an operations_batch context manager that allows batches of modifying operations to be nested. Closes #44.
2 files changed with 75 insertions and 18 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/cart.py
Show inline comments
 
import collections
 
import contextlib
 
import datetime
 
import functools
 
import itertools
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
...
 
@@ -20,18 +21,25 @@ from .discount import DiscountController
 
from .flag import FlagController
 
from .product import ProductController
 

	
 

	
 
def _modifies_cart(func):
 
    ''' Decorator that makes the wrapped function raise ValidationError
 
    if we're doing something that could modify the cart. '''
 
    if we're doing something that could modify the cart.
 

	
 
    It also wraps the execution of this function in a database transaction,
 
    and marks the boundaries of a cart operations batch.
 
    '''
 

	
 
    @functools.wraps(func)
 
    def inner(self, *a, **k):
 
        self._fail_if_cart_is_not_active()
 
        return func(self, *a, **k)
 
        with transaction.atomic():
 
            with CartController.operations_batch(self.cart.user) as mark:
 
                mark.mark = True  # Marker that we've modified the cart
 
                return func(self, *a, **k)
 

	
 
    return inner
 

	
 

	
 
class CartController(object):
 

	
...
 
@@ -53,19 +61,63 @@ class CartController(object):
 
                user=user,
 
                time_last_updated=timezone.now(),
 
                reservation_duration=datetime.timedelta(),
 
            )
 
        return cls(existing)
 

	
 

	
 
    # Marks the carts that are currently in batches
 
    _BATCH_COUNT = collections.defaultdict(int)
 
    _MODIFIED_CARTS = set()
 

	
 
    class _ModificationMarker(object):
 
        pass
 

	
 
    @classmethod
 
    @contextlib.contextmanager
 
    def operations_batch(cls, user):
 
        ''' Marks the boundary for a batch of operations on a user's cart.
 

	
 
        These markers can be nested. Only on exiting the outermost marker will
 
        a batch be ended.
 

	
 
        When a batch is ended, discounts are recalculated, and the cart's
 
        revision is increased.
 
        '''
 

	
 
        ctrl = cls.for_user(user)
 
        _id = ctrl.cart.id
 

	
 
        cls._BATCH_COUNT[_id] += 1
 
        try:
 
            success = False
 

	
 
            marker = cls._ModificationMarker()
 
            yield marker
 

	
 
            if hasattr(marker, "mark"):
 
                cls._MODIFIED_CARTS.add(_id)
 

	
 
            success = True
 
        finally:
 

	
 
            cls._BATCH_COUNT[_id] -= 1
 

	
 
            # Only end on the outermost batch marker, and only if
 
            # it excited cleanly, and a modification occurred
 
            modified = _id in cls._MODIFIED_CARTS
 
            if modified and cls._BATCH_COUNT[_id] == 0 and success:
 
                ctrl._end_batch()
 
                cls._MODIFIED_CARTS.remove(_id)
 

	
 
    def _fail_if_cart_is_not_active(self):
 
        self.cart.refresh_from_db()
 
        if self.cart.status != commerce.Cart.STATUS_ACTIVE:
 
            raise ValidationError("You can only amend active carts.")
 

	
 
    @_modifies_cart
 
    def extend_reservation(self):
 
    def _autoextend_reservation(self):
 
        ''' Updates the cart's time last updated value, which is used to
 
        determine whether the cart has reserved the items and discounts it
 
        holds. '''
 

	
 
        reservations = [datetime.timedelta()]
 

	
...
 
@@ -81,27 +133,32 @@ class CartController(object):
 
        if product_max is not None:
 
            reservations.append(product_max)
 

	
 
        self.cart.time_last_updated = timezone.now()
 
        self.cart.reservation_duration = max(reservations)
 

	
 
    @_modifies_cart
 
    def end_batch(self):
 
    def _end_batch(self):
 
        ''' Performs operations that occur occur at the end of a batch of
 
        product changes/voucher applications etc.
 
        THIS SHOULD BE PRIVATE
 

	
 
        You need to call this after you've finished modifying the user's cart.
 
        This is normally done by wrapping a block of code using
 
        ``operations_batch``.
 

	
 
        '''
 

	
 
        self.recalculate_discounts()
 

	
 
        self.extend_reservation()
 
        self.cart.refresh_from_db()
 

	
 
        self._recalculate_discounts()
 

	
 
        self._autoextend_reservation()
 
        self.cart.revision += 1
 
        self.cart.save()
 

	
 
    @_modifies_cart
 
    @transaction.atomic
 
    def set_quantities(self, product_quantities):
 
        ''' Sets the quantities on each of the products on each of the
 
        products specified. Raises an exception (ValidationError) if a limit
 
        is violated. `product_quantities` is an iterable of (product, quantity)
 
        pairs. '''
 

	
...
 
@@ -137,14 +194,12 @@ class CartController(object):
 
                    product=product,
 
                    quantity=quantity,
 
                )
 

	
 
        items_in_cart.filter(quantity=0).delete()
 

	
 
        self.end_batch()
 

	
 
    def _test_limits(self, product_quantities):
 
        ''' Tests that the quantity changes we intend to make do not violate
 
        the limits and flag conditions imposed on the products. '''
 

	
 
        errors = []
 

	
...
 
@@ -210,13 +265,12 @@ class CartController(object):
 
            return
 

	
 
        self._test_voucher(voucher)
 

	
 
        # If successful...
 
        self.cart.vouchers.add(voucher)
 
        self.end_batch()
 

	
 
    def _test_voucher(self, voucher):
 
        ''' Tests whether this voucher is allowed to be applied to this cart.
 
        Raises ValidationError if not. '''
 

	
 
        # Is voucher exhausted?
...
 
@@ -328,13 +382,12 @@ class CartController(object):
 
                )
 

	
 
        if errors:
 
            raise ValidationError(errors)
 

	
 
    @_modifies_cart
 
    @transaction.atomic
 
    def fix_simple_errors(self):
 
        ''' This attempts to fix the easy errors raised by ValidationError.
 
        This includes removing items from the cart that are no longer
 
        available, recalculating all of the discounts, and removing voucher
 
        codes that are no longer available. '''
 

	
...
 
@@ -360,17 +413,15 @@ class CartController(object):
 

	
 
        not_available = products - available
 
        zeros = [(product, 0) for product in not_available]
 

	
 
        self.set_quantities(zeros)
 

	
 
    @_modifies_cart
 
    @transaction.atomic
 
    def recalculate_discounts(self):
 
        ''' Calculates all of the discounts available for this product.
 
        '''
 
    def _recalculate_discounts(self):
 
        ''' Calculates all of the discounts available for this product.'''
 

	
 
        # Delete the existing entries.
 
        commerce.DiscountItem.objects.filter(cart=self.cart).delete()
 

	
 
        product_items = self.cart.productitem_set.all().select_related(
 
            "product", "product__category", "product__price"
registrasion/controllers/invoice.py
Show inline comments
...
 
@@ -26,12 +26,13 @@ class InvoiceController(ForId, object):
 
    @classmethod
 
    def for_cart(cls, cart):
 
        ''' Returns an invoice object for a given cart at its current revision.
 
        If such an invoice does not exist, the cart is validated, and if valid,
 
        an invoice is generated.'''
 

	
 
        cart.refresh_from_db()
 
        try:
 
            invoice = commerce.Invoice.objects.exclude(
 
                status=commerce.Invoice.STATUS_VOID,
 
            ).get(
 
                cart=cart,
 
                cart_revision=cart.revision,
...
 
@@ -71,12 +72,14 @@ class InvoiceController(ForId, object):
 

	
 
    @classmethod
 
    @transaction.atomic
 
    def _generate(cls, cart):
 
        ''' Generates an invoice for the given cart. '''
 

	
 
        cart.refresh_from_db()
 

	
 
        issued = timezone.now()
 
        reservation_limit = cart.reservation_duration + cart.time_last_updated
 
        # Never generate a due time that is before the issue time
 
        due = max(issued, reservation_limit)
 

	
 
        # Get the invoice recipient
...
 
@@ -248,12 +251,15 @@ class InvoiceController(ForId, object):
 
        self.invoice.status = commerce.Invoice.STATUS_VOID
 
        self.invoice.save()
 

	
 
    def _invoice_matches_cart(self):
 
        ''' Returns true if there is no cart, or if the revision of this
 
        invoice matches the current revision of the cart. '''
 

	
 
        self._refresh()
 

	
 
        cart = self.invoice.cart
 
        if not cart:
 
            return True
 

	
 
        return cart.revision == self.invoice.cart_revision
 

	
0 comments (0 inline, 0 general)