Changeset - 87e6fa064a11
[Not reviewed]
Merge
1 7 3
Christopher Neugebauer - 8 years ago 2016-04-12 01:39:54
chrisjrn@gmail.com
Merge branch 'master' into admin_models_cleanup
9 files changed with 286 insertions and 143 deletions:
0 comments (0 inline, 0 general)
registrasion/admin.py
Show inline comments
...
 
@@ -54,135 +54,135 @@ class DiscountForCategoryInline(admin.TabularInline):
 
class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 
    list_display = (
 
        "description",
 
        "start_time",
 
        "end_time",
 
        "limit",
 
        "effects",
 
    )
 
    ordering = ("start_time", "end_time", "limit")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
@admin.register(rego.IncludedProductDiscount)
 
class IncludedProductDiscountAdmin(admin.ModelAdmin):
 

	
 
    def enablers(self, obj):
 
        return list(obj.enabling_products.all())
 

	
 
    def effects(self, obj):
 
        return list(obj.effects())
 

	
 
    list_display = ("description", "enablers", "effects")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
# Vouchers
 

	
 
class VoucherDiscountInline(nested_admin.NestedStackedInline):
 
    model = rego.VoucherDiscount
 
    verbose_name = _("Discount")
 

	
 
    # TODO work out why we're allowed to add more than one?
 
    max_num = 1
 
    extra = 1
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
class VoucherEnablingConditionInline(nested_admin.NestedStackedInline):
 
    model = rego.VoucherEnablingCondition
 
class VoucherFlagInline(nested_admin.NestedStackedInline):
 
    model = rego.VoucherFlag
 
    verbose_name = _("Product and category enabled by voucher")
 
    verbose_name_plural = _("Products and categories enabled by voucher")
 

	
 
    # TODO work out why we're allowed to add more than one?
 
    max_num = 1
 
    extra = 1
 

	
 

	
 
@admin.register(rego.Voucher)
 
class VoucherAdmin(nested_admin.NestedAdmin):
 

	
 
    def effects(self, obj):
 
        ''' List the effects of the voucher in the admin. '''
 
        out = []
 

	
 
        try:
 
            discount_effects = obj.voucherdiscount.effects()
 
        except ObjectDoesNotExist:
 
            discount_effects = None
 

	
 
        try:
 
            enabling_effects = obj.voucherenablingcondition.effects()
 
            enabling_effects = obj.voucherflag.effects()
 
        except ObjectDoesNotExist:
 
            enabling_effects = None
 

	
 
        if discount_effects:
 
            out.append("Discounts: " + str(list(discount_effects)))
 
        if enabling_effects:
 
            out.append("Enables: " + str(list(enabling_effects)))
 

	
 
        return "\n".join(out)
 

	
 
    model = rego.Voucher
 
    list_display = ("recipient", "code", "effects")
 
    inlines = [
 
        VoucherDiscountInline,
 
        VoucherEnablingConditionInline,
 
        VoucherFlagInline,
 
    ]
 

	
 

	
 
# Enabling conditions
 
@admin.register(rego.ProductEnablingCondition)
 
class ProductEnablingConditionAdmin(
 
@admin.register(rego.ProductFlag)
 
class ProductFlagAdmin(
 
        nested_admin.NestedAdmin,
 
        EffectsDisplayMixin):
 

	
 
    def enablers(self, obj):
 
        return list(obj.enabling_products.all())
 

	
 
    model = rego.ProductEnablingCondition
 
    model = rego.ProductFlag
 
    fields = ("description", "enabling_products", "mandatory", "products",
 
              "categories"),
 

	
 
    list_display = ("description", "enablers", "effects")
 

	
 

	
 
# Enabling conditions
 
@admin.register(rego.CategoryEnablingCondition)
 
class CategoryEnablingConditionAdmin(
 
@admin.register(rego.CategoryFlag)
 
class CategoryFlagAdmin(
 
        nested_admin.NestedAdmin,
 
        EffectsDisplayMixin):
 

	
 
    model = rego.CategoryEnablingCondition
 
    model = rego.CategoryFlag
 
    fields = ("description", "enabling_category", "mandatory", "products",
 
              "categories"),
 

	
 
    list_display = ("description", "enabling_category", "effects")
 
    ordering = ("enabling_category",)
 

	
 

	
 
# Enabling conditions
 
@admin.register(rego.TimeOrStockLimitEnablingCondition)
 
class TimeOrStockLimitEnablingConditionAdmin(
 
@admin.register(rego.TimeOrStockLimitFlag)
 
class TimeOrStockLimitFlagAdmin(
 
        nested_admin.NestedAdmin,
 
        EffectsDisplayMixin):
 
    model = rego.TimeOrStockLimitEnablingCondition
 
    model = rego.TimeOrStockLimitFlag
 

	
 
    list_display = (
 
        "description",
 
        "start_time",
 
        "end_time",
 
        "limit",
 
        "effects",
 
    )
 
    ordering = ("start_time", "end_time", "limit")
registrasion/controllers/cart.py
Show inline comments
...
 
@@ -73,139 +73,139 @@ class CartController(object):
 
        self.cart.save()
 

	
 
    @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. '''
 

	
 
        items_in_cart = rego.ProductItem.objects.filter(cart=self.cart)
 
        items_in_cart = items_in_cart.select_related(
 
            "product",
 
            "product__category",
 
        )
 

	
 
        product_quantities = list(product_quantities)
 

	
 
        # n.b need to add have the existing items first so that the new
 
        # items override the old ones.
 
        all_product_quantities = dict(itertools.chain(
 
            ((i.product, i.quantity) for i in items_in_cart.all()),
 
            product_quantities,
 
        )).items()
 

	
 
        # Validate that the limits we're adding are OK
 
        self._test_limits(all_product_quantities)
 

	
 
        for product, quantity in product_quantities:
 
            try:
 
                product_item = rego.ProductItem.objects.get(
 
                    cart=self.cart,
 
                    product=product,
 
                )
 
                product_item.quantity = quantity
 
                product_item.save()
 
            except ObjectDoesNotExist:
 
                rego.ProductItem.objects.create(
 
                    cart=self.cart,
 
                    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 enabling conditions imposed on the products. '''
 
        the limits and flag conditions imposed on the products. '''
 

	
 
        errors = []
 

	
 
        # Test each product limit here
 
        for product, quantity in product_quantities:
 
            if quantity < 0:
 
                errors.append((product, "Value must be zero or greater."))
 

	
 
            prod = ProductController(product)
 
            limit = prod.user_quantity_remaining(self.cart.user)
 

	
 
            if quantity > limit:
 
                errors.append((
 
                    product,
 
                    "You may only have %d of product: %s" % (
 
                        limit, product,
 
                    )
 
                ))
 

	
 
        # Collect by category
 
        by_cat = collections.defaultdict(list)
 
        for product, quantity in product_quantities:
 
            by_cat[product.category].append((product, quantity))
 

	
 
        # Test each category limit here
 
        for category in by_cat:
 
            ctrl = CategoryController(category)
 
            limit = ctrl.user_quantity_remaining(self.cart.user)
 

	
 
            # Get the amount so far in the cart
 
            to_add = sum(i[1] for i in by_cat[category])
 

	
 
            if to_add > limit:
 
                errors.append((
 
                    category,
 
                    "You may only have %d items in category: %s" % (
 
                        limit, category.name,
 
                    )
 
                ))
 

	
 
        # Test the enabling conditions
 
        errs = ConditionController.test_enabling_conditions(
 
        # Test the flag conditions
 
        errs = ConditionController.test_flags(
 
            self.cart.user,
 
            product_quantities=product_quantities,
 
        )
 

	
 
        if errs:
 
            for error in errs:
 
                errors.append(error)
 

	
 
        if errors:
 
            raise CartValidationError(errors)
 

	
 
    def apply_voucher(self, voucher_code):
 
        ''' Applies the voucher with the given code to this cart. '''
 

	
 
        # Try and find the voucher
 
        voucher = rego.Voucher.objects.get(code=voucher_code.upper())
 

	
 
        # Re-applying vouchers should be idempotent
 
        if voucher in self.cart.vouchers.all():
 
            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?
 
        active_carts = rego.Cart.reserved_carts()
 

	
 
        # It's invalid for a user to enter a voucher that's exhausted
 
        carts_with_voucher = active_carts.filter(vouchers=voucher)
 
        carts_with_voucher = carts_with_voucher.exclude(pk=self.cart.id)
 
        if carts_with_voucher.count() >= voucher.limit:
 
            raise ValidationError(
 
                "Voucher %s is no longer available" % voucher.code)
 

	
 
        # It's not valid for users to re-enter a voucher they already have
 
        user_carts_with_voucher = carts_with_voucher.filter(
 
            user=self.cart.user,
 
        )
 

	
 
        if user_carts_with_voucher.count() > 0:
 
            raise ValidationError("You have already entered this voucher.")
registrasion/controllers/conditions.py
Show inline comments
 
import itertools
 
import operator
 

	
 
from collections import defaultdict
 
from collections import namedtuple
 

	
 
from django.db.models import Sum
 
from django.utils import timezone
 

	
 
from registrasion import models as rego
 

	
 

	
 
ConditionAndRemainder = namedtuple(
 
    "ConditionAndRemainder",
 
    (
 
        "condition",
 
        "remainder",
 
    ),
 
)
 

	
 

	
 
class ConditionController(object):
 
    ''' Base class for testing conditions that activate EnablingCondition
 
    ''' Base class for testing conditions that activate Flag
 
    or Discount objects. '''
 

	
 
    def __init__(self):
 
        pass
 

	
 
    @staticmethod
 
    def for_condition(condition):
 
        CONTROLLERS = {
 
            rego.CategoryEnablingCondition: CategoryConditionController,
 
            rego.CategoryFlag: CategoryConditionController,
 
            rego.IncludedProductDiscount: ProductConditionController,
 
            rego.ProductEnablingCondition: ProductConditionController,
 
            rego.ProductFlag: ProductConditionController,
 
            rego.TimeOrStockLimitDiscount:
 
                TimeOrStockLimitDiscountController,
 
            rego.TimeOrStockLimitEnablingCondition:
 
                TimeOrStockLimitEnablingConditionController,
 
            rego.TimeOrStockLimitFlag:
 
                TimeOrStockLimitFlagController,
 
            rego.VoucherDiscount: VoucherConditionController,
 
            rego.VoucherEnablingCondition: VoucherConditionController,
 
            rego.VoucherFlag: VoucherConditionController,
 
        }
 

	
 
        try:
 
            return CONTROLLERS[type(condition)](condition)
 
        except KeyError:
 
            return ConditionController()
 

	
 
    SINGLE = True
 
    PLURAL = False
 
    NONE = True
 
    SOME = False
 
    MESSAGE = {
 
        NONE: {
 
            SINGLE:
 
                "%(items)s is no longer available to you",
 
            PLURAL:
 
                "%(items)s are no longer available to you",
 
        },
 
        SOME: {
 
            SINGLE:
 
                "Only %(remainder)d of the following item remains: %(items)s",
 
            PLURAL:
 
                "Only %(remainder)d of the following items remain: %(items)s"
 
        },
 
    }
 

	
 
    @classmethod
 
    def test_enabling_conditions(
 
    def test_flags(
 
            cls, user, products=None, product_quantities=None):
 
        ''' Evaluates all of the enabling conditions on the given products.
 
        ''' Evaluates all of the flag conditions on the given products.
 

	
 
        If `product_quantities` is supplied, the condition is only met if it
 
        will permit the sum of the product quantities for all of the products
 
        it covers. Otherwise, it will be met if at least one item can be
 
        accepted.
 

	
 
        If all enabling conditions pass, an empty list is returned, otherwise
 
        If all flag conditions pass, an empty list is returned, otherwise
 
        a list is returned containing all of the products that are *not
 
        enabled*. '''
 

	
 
        if products is not None and product_quantities is not None:
 
            raise ValueError("Please specify only products or "
 
                             "product_quantities")
 
        elif products is None:
 
            products = set(i[0] for i in product_quantities)
 
            quantities = dict((product, quantity)
 
                              for product, quantity in product_quantities)
 
        elif product_quantities is None:
 
            products = set(products)
 
            quantities = {}
 

	
 
        # Get the conditions covered by the products themselves
 

	
 
        prods = (
 
            product.enablingconditionbase_set.select_subclasses()
 
            product.flagbase_set.select_subclasses()
 
            for product in products
 
        )
 
        # Get the conditions covered by their categories
 
        cats = (
 
            category.enablingconditionbase_set.select_subclasses()
 
            category.flagbase_set.select_subclasses()
 
            for category in set(product.category for product in products)
 
        )
 

	
 
        if products:
 
            # Simplify the query.
 
            all_conditions = reduce(operator.or_, itertools.chain(prods, cats))
 
        else:
 
            all_conditions = []
 

	
 
        # All mandatory conditions on a product need to be met
 
        mandatory = defaultdict(lambda: True)
 
        # At least one non-mandatory condition on a product must be met
 
        # if there are no mandatory conditions
 
        non_mandatory = defaultdict(lambda: False)
 
        # All disable-if-false conditions on a product need to be met
 
        do_not_disable = defaultdict(lambda: True)
 
        # At least one enable-if-true condition on a product must be met
 
        do_enable = defaultdict(lambda: False)
 
        # (if either sort of condition is present)
 

	
 
        messages = {}
 

	
 
        for condition in all_conditions:
 
            cond = cls.for_condition(condition)
 
            remainder = cond.user_quantity_remaining(user)
 

	
 
            # Get all products covered by this condition, and the products
 
            # from the categories covered by this condition
 
            cond_products = condition.products.all()
 
            from_category = rego.Product.objects.filter(
 
                category__in=condition.categories.all(),
 
            ).all()
 
            all_products = cond_products | from_category
 
            all_products = all_products.select_related("category")
 
            # Remove the products that we aren't asking about
 
            all_products = [
 
                product
 
                for product in all_products
 
                if product in products
 
            ]
 

	
 
            if quantities:
 
                consumed = sum(quantities[i] for i in all_products)
 
            else:
 
                consumed = 1
 
            met = consumed <= remainder
 

	
 
            if not met:
 
                items = ", ".join(str(product) for product in all_products)
 
                base = cls.MESSAGE[remainder == 0][len(all_products) == 1]
 
                message = base % {"items": items, "remainder": remainder}
 

	
 
            for product in all_products:
 
                if condition.mandatory:
 
                    mandatory[product] &= met
 
                if condition.is_disable_if_false:
 
                    do_not_disable[product] &= met
 
                else:
 
                    non_mandatory[product] |= met
 
                    do_enable[product] |= met
 

	
 
                if not met and product not in messages:
 
                    messages[product] = message
 

	
 
        valid = defaultdict(lambda: True)
 
        for product in itertools.chain(mandatory, non_mandatory):
 
            if product in mandatory:
 
                # If there's a mandatory condition, all must be met
 
                valid[product] = mandatory[product]
 
            else:
 
                # Otherwise, we need just one non-mandatory condition met
 
                valid[product] = non_mandatory[product]
 
        valid = {}
 
        for product in itertools.chain(do_not_disable, do_enable):
 
            if product in do_enable:
 
                # If there's an enable-if-true, we need need of those met too.
 
                # (do_not_disable will default to true otherwise)
 
                valid[product] = do_not_disable[product] and do_enable[product]
 
            elif product in do_not_disable:
 
                # If there's a disable-if-false condition, all must be met
 
                valid[product] = do_not_disable[product]
 

	
 
        error_fields = [
 
            (product, messages[product])
 
            for product in valid if not valid[product]
 
        ]
 

	
 
        return error_fields
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the number of items covered by this enabling condition the
 
        ''' Returns the number of items covered by this flag condition the
 
        user can add to the current cart. This default implementation returns
 
        a big number if is_met() is true, otherwise 0.
 

	
 
        Either this method, or is_met() must be overridden in subclasses.
 
        '''
 

	
 
        return 99999999 if self.is_met(user) else 0
 

	
 
    def is_met(self, user):
 
        ''' Returns True if this enabling condition is met, otherwise returns
 
        ''' Returns True if this flag condition is met, otherwise returns
 
        False.
 

	
 
        Either this method, or user_quantity_remaining() must be overridden
 
        in subclasses.
 
        '''
 
        return self.user_quantity_remaining(user) > 0
 

	
 

	
 
class CategoryConditionController(ConditionController):
 

	
 
    def __init__(self, condition):
 
        self.condition = condition
 

	
 
    def is_met(self, user):
 
        ''' returns True if the user has a product from a category that invokes
 
        this condition in one of their carts '''
 

	
 
        carts = rego.Cart.objects.filter(user=user, released=False)
 
        enabling_products = rego.Product.objects.filter(
 
            category=self.condition.enabling_category,
 
        )
 
        products_count = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product__in=enabling_products,
 
        ).count()
 
        return products_count > 0
 

	
 

	
 
class ProductConditionController(ConditionController):
 
    ''' Condition tests for ProductEnablingCondition and
 
    ''' Condition tests for ProductFlag and
 
    IncludedProductDiscount. '''
 

	
 
    def __init__(self, condition):
 
        self.condition = condition
 

	
 
    def is_met(self, user):
 
        ''' returns True if the user has a product that invokes this
 
        condition in one of their carts '''
 

	
 
        carts = rego.Cart.objects.filter(user=user, released=False)
 
        products_count = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product__in=self.condition.enabling_products.all(),
 
        ).count()
 
        return products_count > 0
 

	
 

	
 
class TimeOrStockLimitConditionController(ConditionController):
 
    ''' Common condition tests for TimeOrStockLimit EnablingCondition and
 
    ''' Common condition tests for TimeOrStockLimit Flag and
 
    Discount.'''
 

	
 
    def __init__(self, ceiling):
 
        self.ceiling = ceiling
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' returns 0 if the date range is violated, otherwise, it will return
 
        the quantity remaining under the stock limit. '''
 

	
 
        # Test date range
 
        if not self._test_date_range():
 
            return 0
 

	
 
        return self._get_remaining_stock(user)
 

	
 
    def _test_date_range(self):
 
        now = timezone.now()
 

	
 
        if self.ceiling.start_time is not None:
 
            if now < self.ceiling.start_time:
 
                return False
 

	
 
        if self.ceiling.end_time is not None:
 
            if now > self.ceiling.end_time:
 
                return False
 

	
 
        return True
 

	
 
    def _get_remaining_stock(self, user):
 
        ''' Returns the stock that remains under this ceiling, excluding the
 
        user's current cart. '''
 

	
 
        if self.ceiling.limit is None:
 
            return 99999999
 

	
 
        # We care about all reserved carts, but not the user's current cart
 
        reserved_carts = rego.Cart.reserved_carts()
 
        reserved_carts = reserved_carts.exclude(
 
            user=user,
 
            active=True,
 
        )
 

	
 
        items = self._items()
 
        items = items.filter(cart__in=reserved_carts)
 
        count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 

	
 
        return self.ceiling.limit - count
 

	
 

	
 
class TimeOrStockLimitEnablingConditionController(
 
class TimeOrStockLimitFlagController(
 
        TimeOrStockLimitConditionController):
 

	
 
    def _items(self):
 
        category_products = rego.Product.objects.filter(
 
            category__in=self.ceiling.categories.all(),
 
        )
 
        products = self.ceiling.products.all() | category_products
 

	
 
        product_items = rego.ProductItem.objects.filter(
 
            product__in=products.all(),
 
        )
 
        return product_items
 

	
 

	
 
class TimeOrStockLimitDiscountController(TimeOrStockLimitConditionController):
 

	
 
    def _items(self):
 
        discount_items = rego.DiscountItem.objects.filter(
 
            discount=self.ceiling,
 
        )
 
        return discount_items
 

	
 

	
 
class VoucherConditionController(ConditionController):
 
    ''' Condition test for VoucherEnablingCondition and VoucherDiscount.'''
 
    ''' Condition test for VoucherFlag and VoucherDiscount.'''
 

	
 
    def __init__(self, condition):
 
        self.condition = condition
 

	
 
    def is_met(self, user):
 
        ''' returns True if the user has the given voucher attached. '''
 
        carts_count = rego.Cart.objects.filter(
 
            user=user,
 
            vouchers=self.condition.voucher,
 
        ).count()
 
        return carts_count > 0
registrasion/controllers/product.py
Show inline comments
 
import itertools
 

	
 
from django.db.models import Sum
 
from registrasion import models as rego
 

	
 
from category import CategoryController
 
from conditions import ConditionController
 

	
 

	
 
class ProductController(object):
 

	
 
    def __init__(self, product):
 
        self.product = product
 

	
 
    @classmethod
 
    def available_products(cls, user, category=None, products=None):
 
        ''' Returns a list of all of the products that are available per
 
        enabling conditions from the given categories.
 
        flag conditions from the given categories.
 
        TODO: refactor so that all conditions are tested here and
 
        can_add_with_enabling_conditions calls this method. '''
 
        can_add_with_flags calls this method. '''
 
        if category is None and products is None:
 
            raise ValueError("You must provide products or a category")
 

	
 
        if category is not None:
 
            all_products = rego.Product.objects.filter(category=category)
 
            all_products = all_products.select_related("category")
 
        else:
 
            all_products = []
 

	
 
        if products is not None:
 
            all_products = set(itertools.chain(all_products, products))
 

	
 
        cat_quants = dict(
 
            (
 
                category,
 
                CategoryController(category).user_quantity_remaining(user),
 
            )
 
            for category in set(product.category for product in all_products)
 
        )
 

	
 
        passed_limits = set(
 
            product
 
            for product in all_products
 
            if cat_quants[product.category] > 0
 
            if cls(product).user_quantity_remaining(user) > 0
 
        )
 

	
 
        failed_and_messages = ConditionController.test_enabling_conditions(
 
        failed_and_messages = ConditionController.test_flags(
 
            user, products=passed_limits
 
        )
 
        failed_conditions = set(i[0] for i in failed_and_messages)
 

	
 
        out = list(passed_limits - failed_conditions)
 
        out.sort(key=lambda product: product.order)
 

	
 
        return out
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the quantity of this product that the user add in the
 
        current cart. '''
 

	
 
        prod_limit = self.product.limit_per_user
 

	
 
        if prod_limit is None:
 
            # Don't need to run the remaining queries
 
            return 999999  # We can do better
 

	
 
        carts = rego.Cart.objects.filter(
 
            user=user,
 
            active=False,
 
            released=False,
 
        )
 

	
 
        items = rego.ProductItem.objects.filter(
 
            cart__in=carts,
 
            product=self.product,
 
        )
 

	
 
        prod_count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 

	
 
        return prod_limit - prod_count
registrasion/migrations/0021_auto_20160411_0748_squashed_0024_auto_20160411_2230.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
# Generated by Django 1.9.2 on 2016-04-11 22:46
 
from __future__ import unicode_literals
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('registrasion', '0020_auto_20160411_0258'),
 
    ]
 

	
 
    operations = [
 
        migrations.RenameModel(
 
            old_name='CategoryEnablingCondition',
 
            new_name='CategoryFlag',
 
        ),
 
        migrations.RenameModel(
 
            old_name='ProductEnablingCondition',
 
            new_name='ProductFlag',
 
        ),
 
        migrations.RenameModel(
 
            old_name='TimeOrStockLimitEnablingCondition',
 
            new_name='TimeOrStockLimitFlag',
 
        ),
 
        migrations.RenameModel(
 
            old_name='VoucherEnablingCondition',
 
            new_name='VoucherFlag',
 
        ),
 
        migrations.AlterModelOptions(
 
            name='categoryflag',
 
            options={'verbose_name': 'flag (dependency on product from category)', 'verbose_name_plural': 'flags (dependency on product from category)'},
 
        ),
 
        migrations.AlterModelOptions(
 
            name='productflag',
 
            options={'verbose_name': 'flag (dependency on product)', 'verbose_name_plural': 'flags (dependency on product)'},
 
        ),
 
        migrations.AlterModelOptions(
 
            name='timeorstocklimitflag',
 
            options={'verbose_name': 'flag (time/stock limit)', 'verbose_name_plural': 'flags (time/stock limit)'},
 
        ),
 
        migrations.AlterModelOptions(
 
            name='voucherflag',
 
            options={'verbose_name': 'flag (dependency on voucher)', 'verbose_name_plural': 'flags (dependency on voucher)'},
 
        ),
 
        migrations.AlterField(
 
            model_name='enablingconditionbase',
 
            name='categories',
 
            field=models.ManyToManyField(blank=True, help_text="Categories whose products are affected by this flag's condition.", to=b'registrasion.Category'),
 
        ),
 
        migrations.RenameField(
 
            model_name='enablingconditionbase',
 
            old_name='mandatory',
 
            new_name='condition',
 
        ),
 
        migrations.AlterField(
 
            model_name='enablingconditionbase',
 
            name='condition',
 
            field=models.IntegerField(choices=[(1, 'Disable if false'), (2, 'Enable if true')], default=2, help_text="If there is at least one 'disable if false' flag defined on a product or category, all such flag  conditions must be met. If there is at least one 'enable if true' flag, at least one such condition must be met. If both types of conditions exist on a product, both of these rules apply."),
 
        ),
 
        migrations.AlterField(
 
            model_name='enablingconditionbase',
 
            name='products',
 
            field=models.ManyToManyField(blank=True, help_text="Products affected by this flag's condition.", to=b'registrasion.Product'),
 
        ),
 
        migrations.AlterField(
 
            model_name='enablingconditionbase',
 
            name='categories',
 
            field=models.ManyToManyField(blank=True, help_text="Categories whose products are affected by this flag's condition.", related_name='flagbase_set', to=b'registrasion.Category'),
 
        ),
 
        migrations.AlterField(
 
            model_name='enablingconditionbase',
 
            name='products',
 
            field=models.ManyToManyField(blank=True, help_text="Products affected by this flag's condition.", related_name='flagbase_set', to=b'registrasion.Product'),
 
        ),
 
    ]
registrasion/models.py
Show inline comments
...
 
@@ -334,205 +334,257 @@ class TimeOrStockLimitDiscount(DiscountBase):
 
    limit = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit"),
 
        help_text=_("This discount may only be applied this many times."),
 
    )
 

	
 

	
 
class VoucherDiscount(DiscountBase):
 
    ''' Discounts that are enabled when a voucher code is in the current
 
    cart. '''
 

	
 
    class Meta:
 
        verbose_name = _("discount (enabled by voucher)")
 
        verbose_name_plural = _("discounts (enabled by voucher)")
 

	
 
    voucher = models.OneToOneField(
 
        Voucher,
 
        on_delete=models.CASCADE,
 
        verbose_name=_("Voucher"),
 
        db_index=True,
 
    )
 

	
 

	
 
class IncludedProductDiscount(DiscountBase):
 
    ''' Discounts that are enabled because another product has been purchased.
 
    e.g. A conference ticket includes a free t-shirt. '''
 

	
 
    class Meta:
 
        verbose_name = _("discount (product inclusions)")
 
        verbose_name_plural = _("discounts (product inclusions)")
 

	
 
    enabling_products = models.ManyToManyField(
 
        Product,
 
        verbose_name=_("Including product"),
 
        help_text=_("If one of these products are purchased, the discounts "
 
                    "below will be enabled."),
 
    )
 

	
 

	
 
class RoleDiscount(object):
 
    ''' Discounts that are enabled because the active user has a specific
 
    role. This is for e.g. volunteers who can get a discount ticket. '''
 
    # TODO: implement RoleDiscount
 
    pass
 

	
 

	
 
@python_2_unicode_compatible
 
class EnablingConditionBase(models.Model):
 
class FlagBase(models.Model):
 
    ''' This defines a condition which allows products or categories to
 
    be made visible. If there is at least one mandatory enabling condition
 
    defined on a Product or Category, it will only be enabled if *all*
 
    mandatory conditions are met, otherwise, if there is at least one enabling
 
    condition defined on a Product or Category, it will only be enabled if at
 
    least one condition is met. '''
 
    be made visible, or be prevented from being visible.
 

	
 
    objects = InheritanceManager()
 
    The various subclasses of this can define the conditions that enable
 
    or disable products, by the following rules:
 

	
 
    If there is at least one 'disable if false' flag defined on a product or
 
    category, all such flag conditions must be met. If there is at least one
 
    'enable if true' flag, at least one such condition must be met.
 

	
 
    If both types of conditions exist on a product, both of these rules apply.
 
    '''
 

	
 
    class Meta:
 
        # TODO: make concrete once https://code.djangoproject.com/ticket/26488
 
        # is solved.
 
        abstract = True
 

	
 
    DISABLE_IF_FALSE = 1
 
    ENABLE_IF_TRUE = 2
 

	
 
    def __str__(self):
 
        return self.description
 

	
 
    def effects(self):
 
        ''' Returns all of the items enabled by this condition. '''
 
        ''' Returns all of the items affected by this condition. '''
 
        return itertools.chain(self.products.all(), self.categories.all())
 

	
 
    @property
 
    def is_disable_if_false(self):
 
        return self.condition == FlagBase.DISABLE_IF_FALSE
 

	
 
    @property
 
    def is_enable_if_true(self):
 
        return self.condition == FlagBase.ENABLE_IF_TRUE
 

	
 
    description = models.CharField(max_length=255)
 
    mandatory = models.BooleanField(
 
        default=False,
 
        help_text=_("If there is at least one mandatory condition defined on "
 
                    "a product or category, all such conditions must be met. "
 
                    "Otherwise, at least one non-mandatory condition must be "
 
                    "met."),
 
    condition = models.IntegerField(
 
        default=ENABLE_IF_TRUE,
 
        choices=(
 
            (DISABLE_IF_FALSE, _("Disable if false")),
 
            (ENABLE_IF_TRUE, _("Enable if true")),
 
        ),
 
        help_text=_("If there is at least one 'disable if false' flag "
 
                    "defined on a product or category, all such flag "
 
                    " conditions must be met. If there is at least one "
 
                    "'enable if true' flag, at least one such condition must "
 
                    "be met. If both types of conditions exist on a product, "
 
                    "both of these rules apply."
 
        ),
 
    )
 
    products = models.ManyToManyField(
 
        Product,
 
        blank=True,
 
        help_text=_("Products that are enabled if this condition is met."),
 
        help_text=_("Products affected by this flag's condition."),
 
        related_name="flagbase_set",
 
    )
 
    categories = models.ManyToManyField(
 
        Category,
 
        blank=True,
 
        help_text=_("Categories whose products are enabled if this condition "
 
                    "is met."),
 
        help_text=_("Categories whose products are affected by this flag's "
 
                    "condition."
 
        ),
 
        related_name="flagbase_set",
 
    )
 

	
 

	
 
class TimeOrStockLimitEnablingCondition(EnablingConditionBase):
 
class EnablingConditionBase(FlagBase):
 
    ''' Reifies the abstract FlagBase. This is necessary because django
 
    prevents renaming base classes in migrations. '''
 
    # TODO: remove this, and make subclasses subclass FlagBase once
 
    # https://code.djangoproject.com/ticket/26488 is solved.
 

	
 
    objects = InheritanceManager()
 

	
 

	
 
class TimeOrStockLimitFlag(EnablingConditionBase):
 
    ''' Registration product ceilings '''
 

	
 
    class Meta:
 
        verbose_name = _("ceiling")
 
        verbose_name = _("flag (time/stock limit)")
 
        verbose_name_plural = _("flags (time/stock limit)")
 

	
 
    start_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        help_text=_("Products included in this condition will only be "
 
                    "available after this time."),
 
    )
 
    end_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        help_text=_("Products included in this condition will only be "
 
                    "available before this time."),
 
    )
 
    limit = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        help_text=_("The number of items under this grouping that can be "
 
                    "purchased."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class ProductEnablingCondition(EnablingConditionBase):
 
class ProductFlag(EnablingConditionBase):
 
    ''' The condition is met because a specific product is purchased. '''
 

	
 
    class Meta:
 
        verbose_name = _("flag (dependency on product)")
 
        verbose_name_plural = _("flags (dependency on product)")
 

	
 
    def __str__(self):
 
        return "Enabled by products: " + str(self.enabling_products.all())
 

	
 
    enabling_products = models.ManyToManyField(
 
        Product,
 
        help_text=_("If one of these products are purchased, this condition "
 
                    "is met."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class CategoryEnablingCondition(EnablingConditionBase):
 
class CategoryFlag(EnablingConditionBase):
 
    ''' The condition is met because a product in a particular product is
 
    purchased. '''
 

	
 
    class Meta:
 
        verbose_name = _("flag (dependency on product from category)")
 
        verbose_name_plural = _("flags (dependency on product from category)")
 

	
 
    def __str__(self):
 
        return "Enabled by product in category: " + str(self.enabling_category)
 

	
 
    enabling_category = models.ForeignKey(
 
        Category,
 
        help_text=_("If a product from this category is purchased, this "
 
                    "condition is met."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class VoucherEnablingCondition(EnablingConditionBase):
 
class VoucherFlag(EnablingConditionBase):
 
    ''' The condition is met because a Voucher is present. This is for e.g.
 
    enabling sponsor tickets. '''
 

	
 
    class Meta:
 
        verbose_name = _("flag (dependency on voucher)")
 
        verbose_name_plural = _("flags (dependency on voucher)")
 

	
 
    def __str__(self):
 
        return "Enabled by voucher: %s" % self.voucher
 

	
 
    voucher = models.OneToOneField(Voucher)
 

	
 

	
 
# @python_2_unicode_compatible
 
class RoleEnablingCondition(object):
 
class RoleFlag(object):
 
    ''' The condition is met because the active user has a particular Role.
 
    This is for e.g. enabling Team tickets. '''
 
    # TODO: implement RoleEnablingCondition
 
    # TODO: implement RoleFlag
 
    pass
 

	
 

	
 
# Commerce Models
 

	
 
@python_2_unicode_compatible
 
class Cart(models.Model):
 
    ''' Represents a set of product items that have been purchased, or are
 
    pending purchase. '''
 

	
 
    class Meta:
 
        index_together = [
 
            ("active", "time_last_updated"),
 
            ("active", "released"),
 
            ("active", "user"),
 
            ("released", "user"),
 
        ]
 

	
 
    def __str__(self):
 
        return "%d rev #%d" % (self.id, self.revision)
 

	
 
    user = models.ForeignKey(User)
 
    # ProductItems (foreign key)
 
    vouchers = models.ManyToManyField(Voucher, blank=True)
 
    time_last_updated = models.DateTimeField(
 
        db_index=True,
 
    )
 
    reservation_duration = models.DurationField()
 
    revision = models.PositiveIntegerField(default=1)
 
    active = models.BooleanField(
 
        default=True,
 
        db_index=True,
 
    )
 
    released = models.BooleanField(
 
        default=False,
 
        db_index=True
 
    )  # Refunds etc
 

	
 
    @classmethod
 
    def reserved_carts(cls):
 
        ''' Gets all carts that are 'reserved' '''
 
        return Cart.objects.filter(
 
            (Q(active=True) &
 
                Q(time_last_updated__gt=(
 
                    timezone.now()-F('reservation_duration')
 
                                        ))) |
 
            (Q(active=False) & Q(released=False))
 
        )
registrasion/tests/test_cart.py
Show inline comments
...
 
@@ -50,113 +50,113 @@ class RegistrationCartTestCase(SetTimeMixin, TestCase):
 
        cls.categories = []
 
        for i in xrange(2):
 
            cat = rego.Category.objects.create(
 
                name="Category " + str(i + 1),
 
                description="This is a test category",
 
                order=i,
 
                render_type=rego.Category.RENDER_TYPE_RADIO,
 
                required=False,
 
            )
 
            cat.save()
 
            cls.categories.append(cat)
 

	
 
        cls.CAT_1 = cls.categories[0]
 
        cls.CAT_2 = cls.categories[1]
 

	
 
        cls.products = []
 
        for i in xrange(4):
 
            prod = rego.Product.objects.create(
 
                name="Product " + str(i + 1),
 
                description="This is a test product.",
 
                category=cls.categories[i / 2],  # 2 products per category
 
                price=Decimal("10.00"),
 
                reservation_duration=cls.RESERVATION,
 
                limit_per_user=10,
 
                order=1,
 
            )
 
            prod.save()
 
            cls.products.append(prod)
 

	
 
        cls.PROD_1 = cls.products[0]
 
        cls.PROD_2 = cls.products[1]
 
        cls.PROD_3 = cls.products[2]
 
        cls.PROD_4 = cls.products[3]
 

	
 
        cls.PROD_4.price = Decimal("5.00")
 
        cls.PROD_4.save()
 

	
 
        # Burn through some carts -- this made some past EC tests fail
 
        current_cart = TestingCartController.for_user(cls.USER_1)
 

	
 
        current_cart.next_cart()
 

	
 
        current_cart = TestingCartController.for_user(cls.USER_2)
 

	
 
        current_cart.next_cart()
 

	
 
    @classmethod
 
    def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
 
        limit_ceiling = rego.TimeOrStockLimitFlag.objects.create(
 
            description=name,
 
            mandatory=True,
 
            condition=rego.FlagBase.DISABLE_IF_FALSE,
 
            limit=limit,
 
            start_time=start_time,
 
            end_time=end_time
 
        )
 
        limit_ceiling.save()
 
        limit_ceiling.products.add(cls.PROD_1, cls.PROD_2)
 
        limit_ceiling.save()
 

	
 
    @classmethod
 
    def make_category_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = rego.TimeOrStockLimitEnablingCondition.objects.create(
 
        limit_ceiling = rego.TimeOrStockLimitFlag.objects.create(
 
            description=name,
 
            mandatory=True,
 
            condition=rego.FlagBase.DISABLE_IF_FALSE,
 
            limit=limit,
 
            start_time=start_time,
 
            end_time=end_time
 
        )
 
        limit_ceiling.save()
 
        limit_ceiling.categories.add(cls.CAT_1)
 
        limit_ceiling.save()
 

	
 
    @classmethod
 
    def make_discount_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None,
 
            percentage=100):
 
        limit_ceiling = rego.TimeOrStockLimitDiscount.objects.create(
 
            description=name,
 
            start_time=start_time,
 
            end_time=end_time,
 
            limit=limit,
 
        )
 
        limit_ceiling.save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=limit_ceiling,
 
            product=cls.PROD_1,
 
            percentage=percentage,
 
            quantity=10,
 
        ).save()
 

	
 
    @classmethod
 
    def new_voucher(self, code="VOUCHER", limit=1):
 
        voucher = rego.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code=code,
 
            limit=limit,
 
        )
 
        voucher.save()
 
        return voucher
 

	
 
    @classmethod
 
    def reget(cls, object):
 
        return type(object).objects.get(id=object.id)
 

	
 

	
 
class BasicCartTests(RegistrationCartTestCase):
 

	
 
    def test_get_cart(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        current_cart.next_cart()
 

	
registrasion/tests/test_flag.py
Show inline comments
 
file renamed from registrasion/tests/test_enabling_condition.py to registrasion/tests/test_flag.py
 
import pytz
 

	
 
from django.core.exceptions import ValidationError
 

	
 
from registrasion import models as rego
 
from registrasion.controllers.category import CategoryController
 
from controller_helpers import TestingCartController
 
from registrasion.controllers.product import ProductController
 

	
 
from test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class EnablingConditionTestCases(RegistrationCartTestCase):
 
class FlagTestCases(RegistrationCartTestCase):
 

	
 
    @classmethod
 
    def add_product_enabling_condition(cls, mandatory=False):
 
        ''' Adds a product enabling condition: adding PROD_1 to a cart is
 
    def add_product_flag(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
 
        ''' Adds a product flag condition: adding PROD_1 to a cart is
 
        predicated on adding PROD_2 beforehand. '''
 
        enabling_condition = rego.ProductEnablingCondition.objects.create(
 
        flag = rego.ProductFlag.objects.create(
 
            description="Product condition",
 
            mandatory=mandatory,
 
            condition=condition,
 
        )
 
        enabling_condition.save()
 
        enabling_condition.products.add(cls.PROD_1)
 
        enabling_condition.enabling_products.add(cls.PROD_2)
 
        enabling_condition.save()
 
        flag.save()
 
        flag.products.add(cls.PROD_1)
 
        flag.enabling_products.add(cls.PROD_2)
 
        flag.save()
 

	
 
    @classmethod
 
    def add_product_enabling_condition_on_category(cls, mandatory=False):
 
        ''' Adds a product enabling condition that operates on a category:
 
    def add_product_flag_on_category(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
 
        ''' Adds a product flag condition that operates on a category:
 
        adding an item from CAT_1 is predicated on adding PROD_3 beforehand '''
 
        enabling_condition = rego.ProductEnablingCondition.objects.create(
 
        flag = rego.ProductFlag.objects.create(
 
            description="Product condition",
 
            mandatory=mandatory,
 
            condition=condition,
 
        )
 
        enabling_condition.save()
 
        enabling_condition.categories.add(cls.CAT_1)
 
        enabling_condition.enabling_products.add(cls.PROD_3)
 
        enabling_condition.save()
 
        flag.save()
 
        flag.categories.add(cls.CAT_1)
 
        flag.enabling_products.add(cls.PROD_3)
 
        flag.save()
 

	
 
    def add_category_enabling_condition(cls, mandatory=False):
 
        ''' Adds a category enabling condition: adding PROD_1 to a cart is
 
    def add_category_flag(cls, condition=rego.FlagBase.ENABLE_IF_TRUE):
 
        ''' Adds a category flag condition: adding PROD_1 to a cart is
 
        predicated on adding an item from CAT_2 beforehand.'''
 
        enabling_condition = rego.CategoryEnablingCondition.objects.create(
 
        flag = rego.CategoryFlag.objects.create(
 
            description="Category condition",
 
            mandatory=mandatory,
 
            condition=condition,
 
            enabling_category=cls.CAT_2,
 
        )
 
        enabling_condition.save()
 
        enabling_condition.products.add(cls.PROD_1)
 
        enabling_condition.save()
 
        flag.save()
 
        flag.products.add(cls.PROD_1)
 
        flag.save()
 

	
 
    def test_product_enabling_condition_enables_product(self):
 
        self.add_product_enabling_condition()
 
    def test_product_flag_enables_product(self):
 
        self.add_product_flag()
 

	
 
        # Cannot buy PROD_1 without buying PROD_2
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_product_enabled_by_product_in_previous_cart(self):
 
        self.add_product_enabling_condition()
 
        self.add_product_flag()
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_2, 1)
 

	
 
        current_cart.next_cart()
 

	
 
        # Create new cart and try to add PROD_1
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_product_enabling_condition_enables_category(self):
 
        self.add_product_enabling_condition_on_category()
 
    def test_product_flag_enables_category(self):
 
        self.add_product_flag_on_category()
 

	
 
        # Cannot buy PROD_1 without buying item from CAT_2
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        current_cart.add_to_cart(self.PROD_3, 1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_category_enabling_condition_enables_product(self):
 
        self.add_category_enabling_condition()
 
    def test_category_flag_enables_product(self):
 
        self.add_category_flag()
 

	
 
        # Cannot buy PROD_1 without buying PROD_2
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # PROD_3 is in CAT_2
 
        current_cart.add_to_cart(self.PROD_3, 1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_product_enabled_by_category_in_previous_cart(self):
 
        self.add_category_enabling_condition()
 
        self.add_category_flag()
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_3, 1)
 

	
 
        current_cart.next_cart()
 

	
 
        # Create new cart and try to add PROD_1
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_multiple_non_mandatory_conditions(self):
 
        self.add_product_enabling_condition()
 
        self.add_category_enabling_condition()
 
    def test_multiple_eit_conditions(self):
 
        self.add_product_flag()
 
        self.add_category_flag()
 

	
 
        # User 1 is testing the product enabling condition
 
        # User 1 is testing the product flag condition
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        # Cannot add PROD_1 until a condition is met
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 
        cart_1.add_to_cart(self.PROD_2, 1)
 
        cart_1.add_to_cart(self.PROD_1, 1)
 

	
 
        # User 2 is testing the category enabling condition
 
        # User 2 is testing the category flag condition
 
        cart_2 = TestingCartController.for_user(self.USER_2)
 
        # Cannot add PROD_1 until a condition is met
 
        with self.assertRaises(ValidationError):
 
            cart_2.add_to_cart(self.PROD_1, 1)
 
        cart_2.add_to_cart(self.PROD_3, 1)
 
        cart_2.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_multiple_mandatory_conditions(self):
 
        self.add_product_enabling_condition(mandatory=True)
 
        self.add_category_enabling_condition(mandatory=True)
 
    def test_multiple_dif_conditions(self):
 
        self.add_product_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
 
        self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        # Cannot add PROD_1 until both conditions are met
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 
        cart_1.add_to_cart(self.PROD_2, 1)  # Meets the product condition
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 
        cart_1.add_to_cart(self.PROD_3, 1)  # Meets the category condition
 
        cart_1.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_mandatory_conditions_are_mandatory(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
        self.add_category_enabling_condition(mandatory=True)
 
    def test_eit_and_dif_conditions_work_together(self):
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 
        self.add_category_flag(condition=rego.FlagBase.DISABLE_IF_FALSE)
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        # Cannot add PROD_1 until both conditions are met
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 
        cart_1.add_to_cart(self.PROD_2, 1)  # Meets the product condition
 

	
 
        cart_1.add_to_cart(self.PROD_2, 1)  # Meets the EIT condition
 

	
 
        # Need to meet both conditions before you can add
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 
        cart_1.add_to_cart(self.PROD_3, 1)  # Meets the category condition
 

	
 
        cart_1.set_quantity(self.PROD_2, 0)  # Un-meets the EIT condition
 

	
 
        cart_1.add_to_cart(self.PROD_3, 1)  # Meets the DIF condition
 

	
 
        # Need to meet both conditions before you can add
 
        with self.assertRaises(ValidationError):
 
            cart_1.add_to_cart(self.PROD_1, 1)
 

	
 
        cart_1.add_to_cart(self.PROD_2, 1)  # Meets the EIT condition
 

	
 
        # Now that both conditions are met, we can add the product
 
        cart_1.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_available_products_works_with_no_conditions_set(self):
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            category=self.CAT_1,
 
        )
 

	
 
        self.assertTrue(self.PROD_1 in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            category=self.CAT_2,
 
        )
 

	
 
        self.assertTrue(self.PROD_3 in prods)
 
        self.assertTrue(self.PROD_4 in prods)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            products=[self.PROD_1, self.PROD_2, self.PROD_3, self.PROD_4],
 
        )
 

	
 
        self.assertTrue(self.PROD_1 in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 
        self.assertTrue(self.PROD_3 in prods)
 
        self.assertTrue(self.PROD_4 in prods)
 

	
 
    def test_available_products_on_category_works_when_condition_not_met(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            category=self.CAT_1,
 
        )
 

	
 
        self.assertTrue(self.PROD_1 not in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 

	
 
    def test_available_products_on_category_works_when_condition_is_met(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        cart_1.add_to_cart(self.PROD_2, 1)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            category=self.CAT_1,
 
        )
 

	
 
        self.assertTrue(self.PROD_1 in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 

	
 
    def test_available_products_on_products_works_when_condition_not_met(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            products=[self.PROD_1, self.PROD_2],
 
        )
 

	
 
        self.assertTrue(self.PROD_1 not in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 

	
 
    def test_available_products_on_products_works_when_condition_is_met(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        cart_1.add_to_cart(self.PROD_2, 1)
 

	
 
        prods = ProductController.available_products(
 
            self.USER_1,
 
            products=[self.PROD_1, self.PROD_2],
 
        )
 

	
 
        self.assertTrue(self.PROD_1 in prods)
 
        self.assertTrue(self.PROD_2 in prods)
 

	
 
    def test_category_enabling_condition_fails_if_cart_refunded(self):
 
        self.add_category_enabling_condition(mandatory=False)
 
    def test_category_flag_fails_if_cart_refunded(self):
 
        self.add_category_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_3, 1)
 

	
 
        cart.next_cart()
 

	
 
        cart_2 = TestingCartController.for_user(self.USER_1)
 
        cart_2.add_to_cart(self.PROD_1, 1)
 
        cart_2.set_quantity(self.PROD_1, 0)
 

	
 
        cart.cart.released = True
 
        cart.next_cart()
 

	
 
        with self.assertRaises(ValidationError):
 
            cart_2.set_quantity(self.PROD_1, 1)
 

	
 
    def test_product_enabling_condition_fails_if_cart_refunded(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
    def test_product_flag_fails_if_cart_refunded(self):
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 1)
 

	
 
        cart.next_cart()
 

	
 
        cart_2 = TestingCartController.for_user(self.USER_1)
 
        cart_2.add_to_cart(self.PROD_1, 1)
 
        cart_2.set_quantity(self.PROD_1, 0)
 

	
 
        cart.cart.released = True
 
        cart.next_cart()
 

	
 
        with self.assertRaises(ValidationError):
 
            cart_2.set_quantity(self.PROD_1, 1)
 

	
 
    def test_available_categories(self):
 
        self.add_product_enabling_condition_on_category(mandatory=False)
 
        self.add_product_flag_on_category(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 

	
 
        cats = CategoryController.available_categories(
 
            self.USER_1,
 
        )
 

	
 
        self.assertFalse(self.CAT_1 in cats)
 
        self.assertTrue(self.CAT_2 in cats)
 

	
 
        cart_1.add_to_cart(self.PROD_3, 1)
 

	
 
        cats = CategoryController.available_categories(
 
            self.USER_1,
 
        )
 

	
 
        self.assertTrue(self.CAT_1 in cats)
 
        self.assertTrue(self.CAT_2 in cats)
 

	
 
    def test_validate_cart_when_enabling_conditions_become_unmet(self):
 
        self.add_product_enabling_condition(mandatory=False)
 
    def test_validate_cart_when_flags_become_unmet(self):
 
        self.add_product_flag(condition=rego.FlagBase.ENABLE_IF_TRUE)
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_2, 1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # Should pass
 
        cart.validate_cart()
 

	
 
        cart.set_quantity(self.PROD_2, 0)
 

	
 
        # Should fail
 
        with self.assertRaises(ValidationError):
 
            cart.validate_cart()
 

	
 
    def test_fix_simple_errors_resolves_unavailable_products(self):
 
        self.test_validate_cart_when_enabling_conditions_become_unmet()
 
        self.test_validate_cart_when_flags_become_unmet()
 
        cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # Should just remove all of the unavailable products
 
        cart.fix_simple_errors()
 
        # Should now succeed
 
        cart.validate_cart()
 

	
 
        # Should keep PROD_2 in the cart
 
        items = rego.ProductItem.objects.filter(cart=cart.cart)
 
        self.assertFalse([i for i in items if i.product == self.PROD_1])
 

	
 
    def test_fix_simple_errors_does_not_remove_limited_items(self):
 
        cart = TestingCartController.for_user(self.USER_1)
 

	
 
        cart.add_to_cart(self.PROD_2, 1)
 
        cart.add_to_cart(self.PROD_1, 10)
 

	
 
        # Should just remove all of the unavailable products
 
        cart.fix_simple_errors()
 
        # Should now succeed
 
        cart.validate_cart()
 

	
 
        # Should keep PROD_2 in the cart
 
        # and also PROD_1, which is now exhausted for user.
 
        items = rego.ProductItem.objects.filter(cart=cart.cart)
 
        self.assertTrue([i for i in items if i.product == self.PROD_1])
registrasion/tests/test_voucher.py
Show inline comments
...
 
@@ -13,104 +13,104 @@ from test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class VoucherTestCases(RegistrationCartTestCase):
 

	
 
    def test_apply_voucher(self):
 
        voucher = self.new_voucher()
 

	
 
        self.set_time(datetime.datetime(2015, 01, 01, tzinfo=UTC))
 

	
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        cart_1.apply_voucher(voucher.code)
 
        self.assertIn(voucher, cart_1.cart.vouchers.all())
 

	
 
        # Second user should not be able to apply this voucher (it's exhausted)
 
        cart_2 = TestingCartController.for_user(self.USER_2)
 
        with self.assertRaises(ValidationError):
 
            cart_2.apply_voucher(voucher.code)
 

	
 
        # After the reservation duration
 
        # user 2 should be able to apply voucher
 
        self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
 
        cart_2.apply_voucher(voucher.code)
 

	
 
        cart_2.next_cart()
 

	
 
        # After the reservation duration, even though the voucher has applied,
 
        # it exceeds the number of vouchers available.
 
        self.add_timedelta(rego.Voucher.RESERVATION_DURATION * 2)
 
        with self.assertRaises(ValidationError):
 
            cart_1.validate_cart()
 

	
 
    def test_fix_simple_errors_resolves_unavailable_voucher(self):
 
        self.test_apply_voucher()
 

	
 
        # User has an exhausted voucher leftover from test_apply_voucher
 
        cart_1 = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            cart_1.validate_cart()
 

	
 
        cart_1.fix_simple_errors()
 
        # This should work now.
 
        cart_1.validate_cart()
 

	
 
    def test_voucher_enables_item(self):
 
        voucher = self.new_voucher()
 

	
 
        enabling_condition = rego.VoucherEnablingCondition.objects.create(
 
        flag = rego.VoucherFlag.objects.create(
 
            description="Voucher condition",
 
            voucher=voucher,
 
            mandatory=False,
 
            condition=rego.FlagBase.ENABLE_IF_TRUE,
 
        )
 
        enabling_condition.save()
 
        enabling_condition.products.add(self.PROD_1)
 
        enabling_condition.save()
 
        flag.save()
 
        flag.products.add(self.PROD_1)
 
        flag.save()
 

	
 
        # Adding the product without a voucher will not work
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # Apply the voucher
 
        current_cart.apply_voucher(voucher.code)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_voucher_enables_discount(self):
 
        voucher = self.new_voucher()
 

	
 
        discount = rego.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        discount.save()
 
        rego.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=self.PROD_1,
 
            percentage=Decimal(100),
 
            quantity=1
 
        ).save()
 

	
 
        # Having PROD_1 in place should add a discount
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        self.assertEqual(1, len(current_cart.cart.discountitem_set.all()))
 

	
 
    def test_voucher_codes_unique(self):
 
        self.new_voucher(code="VOUCHER")
 
        with self.assertRaises(IntegrityError):
 
            self.new_voucher(code="VOUCHER")
 

	
 
    def test_multiple_vouchers_work(self):
 
        self.new_voucher(code="VOUCHER1")
 
        self.new_voucher(code="VOUCHER2")
 

	
 
    def test_vouchers_case_insensitive(self):
 
        voucher = self.new_voucher(code="VOUCHeR")
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code.lower())
 

	
 
    def test_voucher_can_only_be_applied_once(self):
 
        voucher = self.new_voucher(limit=2)
 
        current_cart = TestingCartController.for_user(self.USER_1)
0 comments (0 inline, 0 general)