Changeset - a4d684f444e7
[Not reviewed]
0 3 1
Christopher Neugebauer - 8 years ago 2016-04-03 05:25:39
chrisjrn@gmail.com
Raises limits errors in the right parts of the form
4 files changed with 39 insertions and 15 deletions:
0 comments (0 inline, 0 general)
registrasion/controllers/cart.py
Show inline comments
 
import collections
 
import datetime
 
import discount
 
import itertools
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.db import transaction
 
from django.db.models import Max
 
from django.utils import timezone
 

	
 
from registrasion import models as rego
 
from registrasion.exceptions import CartValidationError
 

	
 
from category import CategoryController
 
from conditions import ConditionController
 
from product import ProductController
 

	
 

	
 
class CartController(object):
 

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

	
 
    @classmethod
 
    def for_user(cls, user):
 
        ''' Returns the user's current cart, or creates a new cart
 
        if there isn't one ready yet. '''
 

	
 
        try:
 
            existing = rego.Cart.objects.get(user=user, active=True)
 
        except ObjectDoesNotExist:
 
            existing = rego.Cart.objects.create(
 
                user=user,
 
                time_last_updated=timezone.now(),
 
                reservation_duration=datetime.timedelta(),
 
                 )
 
            existing.save()
 
        return cls(existing)
 

	
 
    def extend_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()]
 

	
 
        # If we have vouchers, we're entitled to an hour at minimum.
 
        if len(self.cart.vouchers.all()) >= 1:
 
            reservations.append(rego.Voucher.RESERVATION_DURATION)
 

	
 
        # Else, it's the maximum of the included products
 
        items = rego.ProductItem.objects.filter(cart=self.cart)
 
        agg = items.aggregate(Max("product__reservation_duration"))
 
        product_max = agg["product__reservation_duration__max"]
 

	
 
        if product_max is not None:
 
            reservations.append(product_max)
 

	
 
        self.cart.time_last_updated = timezone.now()
 
        self.cart.reservation_duration = max(reservations)
...
 
@@ -69,143 +70,152 @@ class CartController(object):
 

	
 
        self.extend_reservation()
 
        self.cart.revision += 1
 
        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)
 
        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. '''
 

	
 
        errors = []
 

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

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

	
 
            if quantity > limit:
 
                # TODO: batch errors
 
                raise ValidationError(
 
                errors.append((
 
                    product,
 
                    "You may only have %d of product: %s" % (
 
                        limit, product.name,
 
                        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 cat in by_cat:
 
            ctrl = CategoryController(cat)
 
        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[cat])
 
            to_add = sum(i[1] for i in by_cat[category])
 

	
 
            if to_add > limit:
 
                # TODO: batch errors
 
                raise ValidationError(
 
                errors.append((
 
                    category,
 
                    "You may only have %d items in category: %s" % (
 
                        limit, cat.name,
 
                        limit, category.name,
 
                    )
 
                )
 
                ))
 

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

	
 
        if errs:
 
            # TODO: batch errors
 
            raise ValidationError("An enabling condition failed")
 
            errors.append(
 
                ("enabling_conditions", "An enabling condition failed")
 
            )
 

	
 
        if errors:
 
            raise CartValidationError(errors)
 

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

	
 
        # Is voucher exhausted?
 
        active_carts = rego.Cart.reserved_carts()
 

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

	
 
        # It's invalid for a user to enter a voucher that's exhausted
 
        carts_with_voucher = active_carts.filter(vouchers=voucher)
 
        if len(carts_with_voucher) >= voucher.limit:
 
            raise ValidationError("This voucher is no longer available")
 

	
 
        # It's not valid for users to re-enter a voucher they already have
 
        user_carts_with_voucher = rego.Cart.objects.filter(
 
            user=self.cart.user,
 
            released=False,
 
            vouchers=voucher,
 
        )
 
        if len(user_carts_with_voucher) > 0:
 
            raise ValidationError("You have already entered this voucher.")
 

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

	
 
    def validate_cart(self):
 
        ''' Determines whether the status of the current cart is valid;
 
        this is normally called before generating or paying an invoice '''
 

	
 
        # TODO: validate vouchers
 

	
 
        items = rego.ProductItem.objects.filter(cart=self.cart)
 

	
 
        product_quantities = list((i.product, i.quantity) for i in items)
 
        self._test_limits(product_quantities)
 

	
 
        # Validate the discounts
 
        discount_items = rego.DiscountItem.objects.filter(cart=self.cart)
 
        seen_discounts = set()
 

	
 
        for discount_item in discount_items:
 
            discount = discount_item.discount
 
            if discount in seen_discounts:
 
                continue
 
            seen_discounts.add(discount)
registrasion/controllers/category.py
Show inline comments
...
 
@@ -8,49 +8,49 @@ class AllProducts(object):
 

	
 

	
 
class CategoryController(object):
 

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

	
 
    @classmethod
 
    def available_categories(cls, user, products=AllProducts):
 
        ''' Returns the categories available to the user. Specify `products` if
 
        you want to restrict to just the categories that hold the specified
 
        products, otherwise it'll do all. '''
 

	
 
        # STOPGAP -- this needs to be elsewhere tbqh
 
        from product import ProductController
 

	
 
        if products is AllProducts:
 
            products = rego.Product.objects.all()
 

	
 
        available = ProductController.available_products(
 
            user,
 
            products=products,
 
        )
 

	
 
        return set(i.category for i in available)
 

	
 
    def user_quantity_remaining(self, user):
 
        ''' Returns the number of items from this category that the user may
 
        add in the current cart. '''
 

	
 
        cat_limit = self.category.limit_per_user
 

	
 
        if cat_limit is None:
 
            # We don't need to waste the following queries
 
            return 99999999
 

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

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

	
 
        cat_count = items.aggregate(Sum("quantity"))["quantity__sum"] or 0
 
        cat_limit - cat_count
 
        return cat_limit - cat_count
registrasion/exceptions.py
Show inline comments
 
new file 100644
 
from django.core.exceptions import ValidationError
 

	
 
class CartValidationError(ValidationError):
 
    pass
registrasion/views.py
Show inline comments
 
import sys
 

	
 
from registrasion import forms
 
from registrasion import models as rego
 
from registrasion.controllers import discount
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.invoice import InvoiceController
 
from registrasion.controllers.product import ProductController
 
from registrasion.exceptions import CartValidationError
 

	
 
from collections import namedtuple
 

	
 
from django.conf import settings
 
from django.contrib.auth.decorators import login_required
 
from django.contrib import messages
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.http import Http404
 
from django.shortcuts import redirect
 
from django.shortcuts import render
 

	
 

	
 
GuidedRegistrationSection = namedtuple(
 
    "GuidedRegistrationSection",
 
    (
 
        "title",
 
        "discounts",
 
        "description",
 
        "form",
 
    )
 
)
 
GuidedRegistrationSection.__new__.__defaults__ = (
 
    (None,) * len(GuidedRegistrationSection._fields)
 
)
 

	
 

	
 
def get_form(name):
 
    dot = name.rindex(".")
 
    mod_name, form_name = name[:dot], name[dot + 1:]
 
    __import__(mod_name)
 
    return getattr(sys.modules[mod_name], form_name)
 

	
 

	
 
@login_required
 
def guided_registration(request, page_id=0):
 
    ''' Goes through the registration process in order,
 
    making sure user sees all valid categories.
 

	
 
    WORK IN PROGRESS: the finalised version of this view will allow
 
    grouping of categories into a specific page. Currently, it just goes
 
    through each category one by one
 
    '''
 

	
 
    next_step = redirect("guided_registration")
 

	
 
    sections = []
 

	
...
 
@@ -276,105 +277,114 @@ def handle_products(request, category, products, prefix):
 
    ProductsForm = forms.ProductsForm(category, products)
 

	
 
    # Create initial data for each of products in category
 
    items = rego.ProductItem.objects.filter(
 
        product__in=products,
 
        cart=current_cart.cart,
 
    )
 
    quantities = []
 
    for product in products:
 
        # Only add items that are enabled.
 
        try:
 
            quantity = items.get(product=product).quantity
 
        except ObjectDoesNotExist:
 
            quantity = 0
 
        quantities.append((product, quantity))
 

	
 
    products_form = ProductsForm(
 
        request.POST or None,
 
        product_quantities=quantities,
 
        prefix=prefix,
 
    )
 

	
 
    if request.method == "POST" and products_form.is_valid():
 
        if products_form.has_changed():
 
            set_quantities_from_products_form(products_form, current_cart)
 

	
 
        # If category is required, the user must have at least one
 
        # in an active+valid cart
 
        if category.required:
 
            carts = rego.Cart.objects.filter(user=request.user)
 
            items = rego.ProductItem.objects.filter(
 
                product__category=category,
 
                cart=carts,
 
            )
 
            if len(items) == 0:
 
                products_form.add_error(
 
                    None,
 
                    "You must have at least one item from this category",
 
                )
 
    handled = False if products_form.errors else True
 

	
 
    discounts = discount.available_discounts(request.user, [], products)
 

	
 
    return products_form, discounts, handled
 

	
 

	
 
def set_quantities_from_products_form(products_form, current_cart):
 

	
 
    quantities = products_form.product_quantities()
 
    quantities = list(products_form.product_quantities())
 
    product_quantities = [
 
        (rego.Product.objects.get(pk=i[0]), i[1]) for i in quantities
 
    ]
 
    field_names = dict(
 
        (i[0][0], i[1][2]) for i in zip(product_quantities, quantities)
 
    )
 

	
 
    try:
 
        current_cart.set_quantities(product_quantities)
 
    except ValidationError as ve:
 
        products_form.add_error(None, ve)
 
    except CartValidationError as ve:
 
        for ve_field in ve.error_list:
 
            product, message = ve_field.message
 
            if product in field_names:
 
                field = field_names[product]
 
            else:
 
                field = None
 
            products_form.add_error(field, message)
 

	
 

	
 
def handle_voucher(request, prefix):
 
    ''' Handles a voucher form in the given request. Returns the voucher
 
    form instance, and whether the voucher code was handled. '''
 

	
 
    voucher_form = forms.VoucherForm(request.POST or None, prefix=prefix)
 
    current_cart = CartController.for_user(request.user)
 

	
 
    if (voucher_form.is_valid() and
 
            voucher_form.cleaned_data["voucher"].strip()):
 

	
 
        voucher = voucher_form.cleaned_data["voucher"]
 
        voucher = rego.Voucher.normalise_code(voucher)
 

	
 
        if len(current_cart.cart.vouchers.filter(code=voucher)) > 0:
 
            # This voucher has already been applied to this cart.
 
            # Do not apply code
 
            handled = False
 
        else:
 
            try:
 
                current_cart.apply_voucher(voucher)
 
            except Exception as e:
 
                voucher_form.add_error("voucher", e)
 
            handled = True
 
    else:
 
        handled = False
 

	
 
    return (voucher_form, handled)
 

	
 

	
 
@login_required
 
def checkout(request):
 
    ''' Runs checkout for the current cart of items, ideally generating an
 
    invoice. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 
    current_invoice = InvoiceController.for_cart(current_cart.cart)
 

	
 
    return redirect("invoice", current_invoice.invoice.id)
 

	
 

	
 
@login_required
 
def invoice(request, invoice_id):
 
    ''' Displays an invoice for a given invoice id. '''
 

	
 
    invoice_id = int(invoice_id)
 
    inv = rego.Invoice.objects.get(pk=invoice_id)
0 comments (0 inline, 0 general)