Changeset - b13e6f7ce2c2
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-03-26 09:01:46
chrisjrn@gmail.com
Factors out voucher form handling into its own function
2 files changed with 43 insertions and 16 deletions:
0 comments (0 inline, 0 general)
registrasion/models.py
Show inline comments
 
from __future__ import unicode_literals
 

	
 
import datetime
 

	
 
from django.core.exceptions import ValidationError
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.contrib.auth.models import User
 
from django.db import models
 
from django.db.models import F, Q
 
from django.utils import timezone
 
from django.utils.encoding import python_2_unicode_compatible
 
from django.utils.translation import ugettext_lazy as _
 
from model_utils.managers import InheritanceManager
 

	
 

	
 
# User models
 

	
 
@python_2_unicode_compatible
 
class Attendee(models.Model):
 
    ''' Miscellaneous user-related data. '''
 

	
 
    def __str__(self):
 
        return "%s" % self.user
 

	
 
    @staticmethod
 
    def get_instance(user):
 
        ''' Returns the instance of attendee for the given user, or creates
 
        a new one. '''
 
        attendees = Attendee.objects.filter(user=user)
 
        if len(attendees) > 0:
 
            return attendees[0]
 
        else:
 
            attendee = Attendee(user=user)
 
            attendee.save()
 
            return attendee
 

	
 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
 
    # Badge/profile is linked
 
    completed_registration = models.BooleanField(default=False)
 
    highest_complete_category = models.IntegerField(default=0)
 

	
 

	
 
@python_2_unicode_compatible
 
class BadgeAndProfile(models.Model):
 
    ''' Information for an attendee's badge and related preferences '''
 

	
 
    def __str__(self):
 
        return "Badge for: %s of %s" % (self.name, self.company)
 

	
 
    @staticmethod
 
    def get_instance(attendee):
 
        ''' Returns either None, or the instance that belongs
 
        to this attendee. '''
 
        try:
 
            return BadgeAndProfile.objects.get(attendee=attendee)
 
        except ObjectDoesNotExist:
 
            return None
 

	
 
    attendee = models.OneToOneField(Attendee, on_delete=models.CASCADE)
 

	
 
    # Things that appear on badge
 
    name = models.CharField(
 
        verbose_name="Your name (for your conference nametag)",
 
        max_length=64,
 
        help_text="Your name, as you'd like it to appear on your badge. ",
 
    )
 
    company = models.CharField(
 
        max_length=64,
 
        help_text="The name of your company, as you'd like it on your badge",
 
        blank=True,
 
    )
 
    free_text_1 = models.CharField(
 
        max_length=64,
 
        verbose_name="Free text line 1",
 
        help_text="A line of free text that will appear on your badge. Use "
 
                  "this for your Twitter handle, IRC nick, your preferred "
 
                  "pronouns or anything else you'd like people to see on "
 
                  "your badge.",
 
        blank=True,
 
    )
 
    free_text_2 = models.CharField(
 
        max_length=64,
 
        verbose_name="Free text line 2",
 
        blank=True,
 
    )
 

	
 
    # Other important Information
 
    name_per_invoice = models.CharField(
 
        verbose_name="Your legal name (for invoicing purposes)",
 
        max_length=64,
 
        help_text="If your legal name is different to the name on your badge, "
 
                  "fill this in, and we'll put it on your invoice. Otherwise, "
 
                  "leave it blank.",
 
        blank=True,
 
        )
 
    of_legal_age = models.BooleanField(
 
        default=False,
 
        verbose_name="18+?",
 
        blank=True,
 
    )
 
    dietary_requirements = models.CharField(
 
        max_length=256,
 
        blank=True,
 
    )
 
    accessibility_requirements = models.CharField(
 
        max_length=256,
 
        blank=True,
 
    )
 
    gender = models.CharField(
 
        max_length=64,
 
        blank=True,
 
    )
 

	
 

	
 
# Inventory Models
 

	
 
@python_2_unicode_compatible
 
class Category(models.Model):
 
    ''' Registration product categories '''
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    RENDER_TYPE_RADIO = 1
 
    RENDER_TYPE_QUANTITY = 2
 

	
 
    CATEGORY_RENDER_TYPES = [
 
        (RENDER_TYPE_RADIO, _("Radio button")),
 
        (RENDER_TYPE_QUANTITY, _("Quantity boxes")),
 
    ]
 

	
 
    name = models.CharField(max_length=65, verbose_name=_("Name"))
 
    description = models.CharField(max_length=255,
 
                                   verbose_name=_("Description"))
 
    required = models.BooleanField(blank=True)
 
    order = models.PositiveIntegerField(verbose_name=("Display order"))
 
    render_type = models.IntegerField(choices=CATEGORY_RENDER_TYPES,
 
                                      verbose_name=_("Render type"))
 

	
 

	
 
@python_2_unicode_compatible
 
class Product(models.Model):
 
    ''' Registration products '''
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    name = models.CharField(max_length=65, verbose_name=_("Name"))
 
    description = models.CharField(max_length=255,
 
                                   verbose_name=_("Description"))
 
    category = models.ForeignKey(Category, verbose_name=_("Product category"))
 
    price = models.DecimalField(max_digits=8,
 
                                decimal_places=2,
 
                                verbose_name=_("Price"))
 
    limit_per_user = models.PositiveIntegerField(
 
        blank=True,
 
        verbose_name=_("Limit per user"))
 
    reservation_duration = models.DurationField(
 
        default=datetime.timedelta(hours=1),
 
        verbose_name=_("Reservation duration"))
 
    order = models.PositiveIntegerField(verbose_name=("Display order"))
 

	
 

	
 
@python_2_unicode_compatible
 
class Voucher(models.Model):
 
    ''' Registration vouchers '''
 

	
 
    # Vouchers reserve a cart for a fixed amount of time, so that
 
    # items may be added without the voucher being swiped by someone else
 
    RESERVATION_DURATION = datetime.timedelta(hours=1)
 

	
 
    def __str__(self):
 
        return "Voucher for %s" % self.recipient
 

	
 
    @classmethod
 
    def normalise_code(cls, code):
 
        return code.upper()
 

	
 
    def save(self, *a, **k):
 
        ''' Normalise the voucher code to be uppercase '''
 
        self.code = self.code.upper()
 
        self.code = self.normalise_code(self.code)
 
        super(Voucher, self).save(*a, **k)
 

	
 
    recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
 
    code = models.CharField(max_length=16,
 
                            unique=True,
 
                            verbose_name=_("Voucher code"))
 
    limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
 

	
 

	
 
# Product Modifiers
 

	
 
@python_2_unicode_compatible
 
class DiscountBase(models.Model):
 
    ''' Base class for discounts. Each subclass has controller code that
 
    determines whether or not the given discount is available to be added to
 
    the current cart. '''
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return "Discount: " + self.description
 

	
 
    description = models.CharField(max_length=255,
 
                                   verbose_name=_("Description"))
 

	
 

	
 
@python_2_unicode_compatible
 
class DiscountForProduct(models.Model):
 
    ''' Represents a discount on an individual product. Each Discount can
 
    contain multiple products and categories. Discounts can either be a
 
    percentage or a fixed amount, but not both. '''
 

	
 
    def __str__(self):
 
        if self.percentage:
 
            return "%s%% off %s" % (self.percentage, self.product)
 
        elif self.price:
 
            return "$%s off %s" % (self.price, self.product)
 

	
 
    def clean(self):
 
        if self.percentage is None and self.price is None:
 
            raise ValidationError(
 
                _("Discount must have a percentage or a price."))
 
        elif self.percentage is not None and self.price is not None:
 
            raise ValidationError(
 
                _("Discount may only have a percentage or only a price."))
 

	
 
        prods = DiscountForProduct.objects.filter(
 
            discount=self.discount,
 
            product=self.product)
 
        cats = DiscountForCategory.objects.filter(
 
            discount=self.discount,
 
            category=self.product.category)
 
        if len(prods) > 1:
 
            raise ValidationError(
 
                _("You may only have one discount line per product"))
 
        if len(cats) != 0:
 
            raise ValidationError(
 
                _("You may only have one discount for "
 
                    "a product or its category"))
 

	
 
    discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
 
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
 
    percentage = models.DecimalField(
 
        max_digits=4, decimal_places=1, null=True, blank=True)
 
    price = models.DecimalField(
 
        max_digits=8, decimal_places=2, null=True, blank=True)
 
    quantity = models.PositiveIntegerField()
 

	
 

	
 
@python_2_unicode_compatible
 
class DiscountForCategory(models.Model):
 
    ''' Represents a discount for a category of products. Each discount can
 
    contain multiple products. Category discounts can only be a percentage. '''
 

	
 
    def __str__(self):
 
        return "%s%% off %s" % (self.percentage, self.category)
 

	
 
    def clean(self):
 
        prods = DiscountForProduct.objects.filter(
 
            discount=self.discount,
 
            product__category=self.category)
 
        cats = DiscountForCategory.objects.filter(
 
            discount=self.discount,
 
            category=self.category)
 
        if len(prods) != 0:
 
            raise ValidationError(
 
                _("You may only have one discount for "
 
                    "a product or its category"))
 
        if len(cats) > 1:
 
            raise ValidationError(
 
                _("You may only have one discount line per category"))
 

	
 
    discount = models.ForeignKey(DiscountBase, on_delete=models.CASCADE)
 
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
 
    percentage = models.DecimalField(
 
        max_digits=4,
 
        decimal_places=1)
 
    quantity = models.PositiveIntegerField()
 

	
 

	
 
class TimeOrStockLimitDiscount(DiscountBase):
 
    ''' Discounts that are generally available, but are limited by timespan or
 
    usage count. This is for e.g. Early Bird discounts. '''
 

	
 
    class Meta:
 
        verbose_name = _("Promotional discount")
 

	
 
    start_time = models.DateTimeField(
 
        null=True, blank=True, verbose_name=_("Start time"))
 
    end_time = models.DateTimeField(
 
        null=True, blank=True, verbose_name=_("End time"))
 
    limit = models.PositiveIntegerField(
 
        null=True, blank=True, verbose_name=_("Limit"))
 

	
 

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

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

	
 

	
 
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 = _("Product inclusion")
 

	
 
    enabling_products = models.ManyToManyField(
 
        Product,
 
        verbose_name=_("Including product"))
 

	
 

	
 
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):
 
    ''' 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. '''
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    description = models.CharField(max_length=255)
 
    mandatory = models.BooleanField(default=False)
 
    products = models.ManyToManyField(Product, blank=True)
 
    categories = models.ManyToManyField(Category, blank=True)
 

	
 

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

	
 
    start_time = models.DateTimeField(null=True, verbose_name=_("Start time"))
 
    end_time = models.DateTimeField(null=True, verbose_name=_("End time"))
 
    limit = models.PositiveIntegerField(null=True, verbose_name=_("Limit"))
 

	
 

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

	
 
    def __str__(self):
 
        return "Enabled by product: "
 

	
 
    enabling_products = models.ManyToManyField(Product)
 

	
 

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

	
 
    def __str__(self):
 
        return "Enabled by product in category: "
 

	
 
    enabling_category = models.ForeignKey(Category)
 

	
registrasion/views.py
Show inline comments
 
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 django.contrib.auth.decorators import login_required
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.db import transaction
 
from django.shortcuts import redirect
 
from django.shortcuts import render
 

	
 

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

	
 
    dashboard = redirect("dashboard")
 
    next_step = redirect("guided_registration")
 

	
 
    attendee = rego.Attendee.get_instance(request.user)
 
    if attendee.completed_registration:
 
        return dashboard
 

	
 
    # Step 1: Fill in a badge
 
    profile = rego.BadgeAndProfile.get_instance(attendee)
 

	
 
    if profile is None:
 
        ret = edit_profile(request)
 
        profile_new = rego.BadgeAndProfile.get_instance(attendee)
 
        if profile_new is None:
 
            # No new profile was created
 
            return ret
 
        else:
 
            return next_step
 

	
 
    # Step 2: Go through each of the categories in order
 
    category = attendee.highest_complete_category
 

	
 
    # Get the next category
 
    cats = rego.Category.objects
 
    cats = cats.filter(id__gt=category).order_by("order")
 

	
 
    if len(cats) == 0:
 
        # We've filled in every category
 
        attendee.completed_registration = True
 
        attendee.save()
 
        return dashboard
 

	
 
    ret = product_category(request, cats[0].id)
 
    attendee_new = rego.Attendee.get_instance(request.user)
 
    if attendee_new.highest_complete_category == category:
 
        # We've not yet completed this category
 
        return ret
 
    else:
 
        return next_step
 

	
 

	
 
@login_required
 
def edit_profile(request):
 
    attendee = rego.Attendee.get_instance(request.user)
 

	
 
    try:
 
        profile = rego.BadgeAndProfile.objects.get(attendee=attendee)
 
    except ObjectDoesNotExist:
 
        profile = None
 

	
 
    form = forms.ProfileForm(request.POST or None, instance=profile)
 

	
 
    if request.POST and form.is_valid():
 
        form.instance.attendee = attendee
 
        form.save()
 

	
 
    data = {
 
        "form": form,
 
    }
 
    return render(request, "profile_form.html", data)
 

	
 

	
 
@login_required
 
def product_category(request, category_id):
 
    ''' Registration selections form for a specific category of items.
 
    '''
 

	
 
    PRODUCTS_FORM_PREFIX = "products"
 
    VOUCHERS_FORM_PREFIX = "vouchers"
 

	
 
    category_id = int(category_id)  # Routing is [0-9]+
 
    category = rego.Category.objects.get(pk=category_id)
 
    current_cart = CartController.for_user(request.user)
 

	
 
    attendee = rego.Attendee.get_instance(request.user)
 

	
 
    # Handle the voucher form *before* listing products.
 
    v = handle_voucher(request, VOUCHERS_FORM_PREFIX)
 
    voucher_form, voucher_handled = v
 
    if voucher_handled:
 
        # Do not handle product form
 
        pass
 

	
 
    products = rego.Product.objects.filter(category=category)
 
    products = products.order_by("order")
 
    products = ProductController.available_products(
 
        request.user,
 
        products=products,
 
    )
 

	
 
    ProductsForm = forms.ProductsForm(products)
 

	
 
    if request.method == "POST":
 
        cat_form = ProductsForm(
 
            request.POST,
 
            request.FILES,
 
            prefix=PRODUCTS_FORM_PREFIX)
 
        voucher_form = forms.VoucherForm(
 
            request.POST,
 
            prefix=VOUCHERS_FORM_PREFIX)
 

	
 
        if (voucher_form.is_valid() and
 
                voucher_form.cleaned_data["voucher"].strip()):
 
            # Apply voucher
 
            # leave
 
            voucher = voucher_form.cleaned_data["voucher"]
 
            try:
 
                current_cart.apply_voucher(voucher)
 
            except Exception as e:
 
                voucher_form.add_error("voucher", e)
 
            # Re-visit current page.
 
        if voucher_handled:
 
            # The voucher form was handled here.
 
            pass
 
        elif cat_form.is_valid():
 
            try:
 
                handle_valid_cat_form(cat_form, current_cart)
 
            except ValidationError:
 
                pass
 

	
 
            # If category is required, the user must have at least one
 
            # in an active+valid cart
 

	
 
            if category.required:
 
                carts = rego.Cart.reserved_carts()
 
                carts = carts.filter(user=request.user)
 
                items = rego.ProductItem.objects.filter(
 
                    product__category=category,
 
                    cart=carts,
 
                )
 
                if len(items) == 0:
 
                    cat_form.add_error(
 
                        None,
 
                        "You must have at least one item from this category",
 
                    )
 

	
 
            if not cat_form.errors:
 
                if category_id > attendee.highest_complete_category:
 
                    attendee.highest_complete_category = category_id
 
                    attendee.save()
 
                return redirect("dashboard")
 

	
 
    else:
 
        # Create initial data for each of products in category
 
        items = rego.ProductItem.objects.filter(
 
            product__category=category,
 
            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))
 

	
 
        cat_form = ProductsForm(
 
            prefix=PRODUCTS_FORM_PREFIX,
 
            product_quantities=quantities,
 
        )
 

	
 
        voucher_form = forms.VoucherForm(prefix=VOUCHERS_FORM_PREFIX)
 

	
 
    discounts = discount.available_discounts(request.user, [], products)
 
    data = {
 
        "category": category,
 
        "discounts": discounts,
 
        "form": cat_form,
 
        "voucher_form": voucher_form,
 
    }
 

	
 
    return render(request, "product_category.html", data)
 

	
 

	
 
@transaction.atomic
 
def handle_valid_cat_form(cat_form, current_cart):
 
    for product_id, quantity, field_name in cat_form.product_quantities():
 
        product = rego.Product.objects.get(pk=product_id)
 
        try:
 
            current_cart.set_quantity(product, quantity, batched=True)
 
        except ValidationError as ve:
 
            cat_form.add_error(field_name, ve)
 
    if cat_form.errors:
 
        raise ValidationError("Cannot add that stuff")
 
    current_cart.end_batch()
 

	
 
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)
 
    current_invoice = InvoiceController(inv)
 

	
 
    data = {
 
        "invoice": current_invoice.invoice,
 
    }
 

	
 
    return render(request, "invoice.html", data)
 

	
 

	
 
@login_required
 
def pay_invoice(request, invoice_id):
 
    ''' Marks the invoice with the given invoice id as paid.
 
    WORK IN PROGRESS FUNCTION. Must be replaced with real payment workflow.
 

	
 
    '''
 

	
 
    invoice_id = int(invoice_id)
 
    inv = rego.Invoice.objects.get(pk=invoice_id)
 
    current_invoice = InvoiceController(inv)
 
    if not inv.paid and current_invoice.is_valid():
 
        current_invoice.pay("Demo invoice payment", inv.value)
 

	
 
    return redirect("invoice", current_invoice.invoice.id)
0 comments (0 inline, 0 general)