Changeset - 89cba55807c1
[Not reviewed]
0 2 0
Christopher Neugebauer - 8 years ago 2016-04-01 10:39:54
chrisjrn@gmail.com
Pre-fills the attendee name from a speaker profile, if there is one.

Resolves #8.
2 files changed with 22 insertions and 2 deletions:
0 comments (0 inline, 0 general)
registrasion/models.py
Show inline comments
 
from __future__ import unicode_literals
 

	
 
import datetime
 
import itertools
 

	
 
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)
 

	
 

	
 
class AttendeeProfileBase(models.Model):
 
    ''' Information for an attendee's badge and related preferences.
 
    Subclass this in your Django site to ask for attendee information in your
 
    registration progess.
 
     '''
 

	
 
    @classmethod
 
    def name_field(cls):
 
        ''' This is used to pre-fill the attendee's name from the
 
        speaker profile. If it's None, that functionality is disabled. '''
 
        return None
 

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

	
 

	
 
# Inventory Models
 

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

	
 
    class Meta:
 
        verbose_name_plural = _("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"),
 
    )
 
    limit_per_user = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
        help_text=_("The total number of items from this category one "
 
                    "attendee may purchase."),
 
    )
 
    required = models.BooleanField(
 
        blank=True,
 
        help_text=_("If enabled, a user must select an "
 
                    "item from this category."),
 
    )
 
    order = models.PositiveIntegerField(
 
        verbose_name=("Display order"),
 
    )
 
    render_type = models.IntegerField(
 
        choices=CATEGORY_RENDER_TYPES,
 
        verbose_name=_("Render type"),
 
        help_text=_("The registration form will render this category in this "
 
                    "style."),
 
    )
 

	
 

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

	
 
    def __str__(self):
 
        return "%s - %s" % (self.category.name, self.name)
 

	
 
    name = models.CharField(
 
        max_length=65,
 
        verbose_name=_("Name"),
 
    )
 
    description = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Description"),
 
        null=True,
 
        blank=True,
 
    )
 
    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(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
    )
 
    reservation_duration = models.DurationField(
 
        default=datetime.timedelta(hours=1),
 
        verbose_name=_("Reservation duration"),
 
        help_text=_("The length of time this product will be reserved before "
 
                    "it is released for someone else to purchase."),
 
    )
 
    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.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
 

	
 
    def effects(self):
 
        ''' Returns all of the effects of this discount. '''
 
        products = self.discountforproduct_set.all()
 
        categories = self.discountforcategory_set.all()
 
        return itertools.chain(products, categories)
 

	
 
    description = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Description"),
 
        help_text=_("A description of this discount. This will be included on "
 
                    "invoices where this discount is applied."),
 
        )
 

	
 

	
 
@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()
 

	
registrasion/views.py
Show inline comments
 
import symposion.speakers
 
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 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.db import transaction
 
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
 
    '''
 

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

	
 
    sections = []
 

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

	
 
    if attendee.completed_registration:
 
        return render(
 
            request,
 
            "registrasion/guided_registration_complete.html",
 
            {},
 
        )
 

	
 
    # Step 1: Fill in a badge and collect a voucher code
 
    try:
 
        profile = attendee.attendeeprofilebase
 
    except ObjectDoesNotExist:
 
        profile = None
 

	
 
    if not profile:
 
        # TODO: if voucherform is invalid, make sure that profileform does not save
 
        voucher_form, voucher_handled = handle_voucher(request, "voucher")
 
        profile_form, profile_handled = handle_profile(request, "profile")
 

	
 
        voucher_section = GuidedRegistrationSection(
 
            title="Voucher Code",
 
            form=voucher_form,
 
        )
 

	
 
        profile_section = GuidedRegistrationSection(
 
            title="Profile and Personal Information",
 
            form=profile_form,
 
        )
 

	
 
        title = "Attendee information"
 
        current_step = 1
 
        sections.append(voucher_section)
 
        sections.append(profile_section)
 
    else:
 
        # We're selling products
 

	
 
        last_category = attendee.highest_complete_category
 

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

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

	
 
        if last_category == 0:
 
            # Only display the first Category
 
            title = "Select ticket type"
 
            current_step = 2
 
            cats = [cats[0]]
 
        else:
 
            # Set title appropriately for remaining categories
 
            current_step = 3
 
            title = "Additional items"
 

	
 
        for category in cats:
 
            products = ProductController.available_products(
 
                request.user,
 
                category=category,
 
            )
 

	
 
            prefix = "category_" + str(category.id)
 
            p = handle_products(request, category, products, prefix)
 
            products_form, discounts, products_handled = p
 

	
 
            section = GuidedRegistrationSection(
 
                title=category.name,
 
                description=category.description,
 
                discounts=discounts,
 
                form=products_form,
 
            )
 
            sections.append(section)
 

	
 
            if request.method == "POST" and not products_form.errors:
 
                if category.id > attendee.highest_complete_category:
 
                    # This is only saved if we pass each form with no errors.
 
                    attendee.highest_complete_category = category.id
 

	
 

	
 
    if sections and request.method == "POST":
 
        for section in sections:
 
            if section.form.errors:
 
                break
 
        else:
 
            attendee.save()
 
            # We've successfully processed everything
 
            return next_step
 

	
 
    data = {
 
        "current_step": current_step,
 
        "sections": sections,
 
        "title" : title,
 
        "total_steps": 3,
 
    }
 
    return render(request, "registrasion/guided_registration.html", data)
 

	
 

	
 
@login_required
 
def edit_profile(request):
 
    form, handled = handle_profile(request, "profile")
 

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

	
 
def handle_profile(request, prefix):
 
    ''' Returns a profile form instance, and a boolean which is true if the
 
    form was handled. '''
 
    attendee = rego.Attendee.get_instance(request.user)
 

	
 
    try:
 
        profile = attendee.attendeeprofilebase
 
    except ObjectDoesNotExist:
 
        profile = None
 

	
 
    # TODO: pull down the speaker's real name from the Speaker profile
 

	
 
    ProfileForm = get_form(settings.ATTENDEE_PROFILE_FORM)
 

	
 
    # Load a pre-entered name from the speaker's profile,
 
    # if they have one.
 
    try:
 
        speaker_profile = request.user.speaker_profile
 
        speaker_name = speaker_profile.name
 
    except ObjectDoesNotExist:
 
        speaker_name = None
 

	
 

	
 
    name_field = ProfileForm.Meta.model.name_field()
 
    initial = {}
 
    if name_field is not None:
 
        initial[name_field] = speaker_name
 

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

	
 
    handled = True if request.POST else False
 

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

	
 
    return form, handled
 

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

	
 
    # Handle the voucher form *before* listing products.
 
    # Products can change as vouchers are entered.
 
    v = handle_voucher(request, VOUCHERS_FORM_PREFIX)
 
    voucher_form, voucher_handled = v
 

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

	
 
    products = ProductController.available_products(
 
        request.user,
 
        category=category,
 
    )
 

	
 
    p = handle_products(request, category, products, PRODUCTS_FORM_PREFIX)
 
    products_form, discounts, products_handled = p
 

	
 
    if request.POST and not voucher_handled and not products_form.errors:
 
        # Only return to the dashboard if we didn't add a voucher code
 
        # and if there's no errors in the products form
 
        return redirect("dashboard")
 

	
 
    data = {
 
        "category": category,
 
        "discounts": discounts,
 
        "form": products_form,
 
        "voucher_form": voucher_form,
 
    }
 

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

	
 

	
 
def handle_products(request, category, products, prefix):
 
    ''' Handles a products list form in the given request. Returns the
 
    form instance, the discounts applicable to this form, and whether the
 
    contents were handled. '''
 

	
 
    current_cart = CartController.for_user(request.user)
 

	
 
    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():
 
        try:
 
            if products_form.has_changed():
 
                set_quantities_from_products_form(products_form, current_cart)
 
        except ValidationError:
 
            # There were errors, but they've already been added to the form.
 
            pass
 

	
 
        # 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
 

	
 

	
 
@transaction.atomic
 
def set_quantities_from_products_form(products_form, current_cart):
 
    # TODO: issue #8 is a problem here.
 
    quantities = list(products_form.product_quantities())
 
    quantities.sort(key=lambda item: item[1])
 
    for product_id, quantity, field_name in quantities:
 
        product = rego.Product.objects.get(pk=product_id)
 
        try:
 
            current_cart.set_quantity(product, quantity, batched=True)
 
        except ValidationError as ve:
 
            products_form.add_error(field_name, ve)
 
    if products_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)
 

	
 
    if request.user != inv.cart.user and not request.user.is_staff:
 
        raise Http404()
 

	
 
    current_invoice = InvoiceController(inv)
 

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

	
 
    return render(request, "registrasion/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)
0 comments (0 inline, 0 general)