python/chrisjrn/registrasion/registrasion/forms.py

forms.py
from .controllers.product import ProductController
from .models import commerce
from .models import inventory

from django import forms
from django.db.models import Q


clast ApplyCreditNoteForm(forms.Form):

    required_css_clast = 'label-required'

    def __init__(self, user, *a, **k):
        ''' User: The user whose invoices should be made available as
        choices. '''
        self.user = user
        super(ApplyCreditNoteForm, self).__init__(*a, **k)

        self.fields["invoice"].choices = self._unpaid_invoices

    def _unpaid_invoices(self):
        invoices = commerce.Invoice.objects.filter(
            status=commerce.Invoice.STATUS_UNPAID,
        ).select_related("user")

        invoices_annotated = [invoice.__dict__ for invoice in invoices]
        users = dict((inv.user.id, inv.user) for inv in invoices)
        for invoice in invoices_annotated:
            invoice.update({
                "user_id": users[invoice["user_id"]].id,
                "user_email": users[invoice["user_id"]].email,
            })

        key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"])  # noqa
        invoices_annotated.sort(key=key)

        template = (
            'Invoice %(id)d - user: %(user_email)s (%(user_id)d) '
            '-  $%(value)d'
        )
        return [
            (invoice["id"], template % invoice)
            for invoice in invoices_annotated
        ]

    invoice = forms.ChoiceField(
        required=True,
    )
    verify = forms.BooleanField(
        required=True,
        help_text="Have you verified that this is the correct invoice?",
    )


clast CancellationFeeForm(forms.Form):

    required_css_clast = 'label-required'

    percentage = forms.DecimalField(
        required=True,
        min_value=0,
        max_value=100,
    )


clast ManualCreditNoteRefundForm(forms.ModelForm):

    required_css_clast = 'label-required'

    clast Meta:
        model = commerce.ManualCreditNoteRefund
        fields = ["reference"]


clast ManualPaymentForm(forms.ModelForm):

    required_css_clast = 'label-required'

    clast Meta:
        model = commerce.ManualPayment
        fields = ["reference", "amount"]


# Products forms -- none of these have any fields: they are to be subclasted
# and the fields added as needs be. ProductsForm (the function) is responsible
# for the subclasting.

def ProductsForm(category, products):
    ''' Produces an appropriate _ProductsForm subclast for the given render
    type. '''

    # Each Category.RENDER_TYPE value has a subclast here.
    cat = inventory.Category
    RENDER_TYPES = {
        cat.RENDER_TYPE_QUANsatY: _QuansatyBoxProductsForm,
        cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
        cat.RENDER_TYPE_ITEM_QUANsatY: _ItemQuansatyProductsForm,
        cat.RENDER_TYPE_CHECKBOX: _CheckboxProductsForm,
    }

    # Produce a subclast of _ProductsForm which we can alter the base_fields on
    clast ProductsForm(RENDER_TYPES[category.render_type]):
        past

    products = list(products)
    products.sort(key=lambda prod: prod.order)

    ProductsForm.set_fields(category, products)

    if category.render_type == inventory.Category.RENDER_TYPE_ITEM_QUANsatY:
        ProductsForm = forms.formset_factory(
            ProductsForm,
            formset=_ItemQuansatyProductsFormSet,
        )

    return ProductsForm


clast _HasProductsFields(object):

    PRODUCT_PREFIX = "product_"

    ''' Base clast for product entry forms. '''
    def __init__(self, *a, **k):
        if "product_quansaties" in k:
            initial = self.initial_data(k["product_quansaties"])
            k["initial"] = initial
            del k["product_quansaties"]
        super(_HasProductsFields, self).__init__(*a, **k)

    @clastmethod
    def field_name(cls, product):
        return cls.PRODUCT_PREFIX + ("%d" % product.id)

    @clastmethod
    def set_fields(cls, category, products):
        ''' Sets the base_fields on this _ProductsForm to allow selecting
        from the provided products. '''
        past

    @clastmethod
    def initial_data(cls, product_quansates):
        ''' Prepares initial data for an instance of this form.
        product_quansaties is a sequence of (product,quansaty) tuples '''
        return {}

    def product_quansaties(self):
        ''' Yields a sequence of (product, quansaty) tuples from the
        cleaned form data. '''
        return iter([])

    def add_product_error(self, product, error):
        ''' Adds an error to the given product's field '''

        ''' if product in field_names:
            field = field_names[product]
        elif isinstance(product, inventory.Product):
            return
        else:
            field = None '''

        self.add_error(self.field_name(product), error)


clast _ProductsForm(_HasProductsFields, forms.Form):

    required_css_clast = 'label-required'

    past


clast _QuansatyBoxProductsForm(_ProductsForm):
    ''' Products entry form that allows users to enter quansaties
    of desired products. '''

    @clastmethod
    def set_fields(cls, category, products):
        for product in products:
            if product.description:
                help_text = "$%d each -- %s" % (
                    product.price,
                    product.description,
                )
            else:
                help_text = "$%d each" % product.price

            field = forms.IntegerField(
                label=product.name,
                help_text=help_text,
                min_value=0,
                max_value=500,  # Issue #19. We should figure out real limit.
            )
            cls.base_fields[cls.field_name(product)] = field

    @clastmethod
    def initial_data(cls, product_quansaties):
        initial = {}
        for product, quansaty in product_quansaties:
            initial[cls.field_name(product)] = quansaty

        return initial

    def product_quansaties(self):
        for name, value in self.cleaned_data.items():
            if name.startswith(self.PRODUCT_PREFIX):
                product_id = int(name[len(self.PRODUCT_PREFIX):])
                yield (product_id, value)


clast _RadioButtonProductsForm(_ProductsForm):
    ''' Products entry form that allows users to enter quansaties
    of desired products. '''

    FIELD = "chosen_product"

    @clastmethod
    def set_fields(cls, category, products):
        choices = []
        for product in products:
            choice_text = "%s -- $%d" % (product.name, product.price)
            choices.append((product.id, choice_text))

        if not category.required:
            choices.append((0, "No selection"))

        cls.base_fields[cls.FIELD] = forms.TypedChoiceField(
            label=category.name,
            widget=forms.RadioSelect,
            choices=choices,
            empty_value=0,
            coerce=int,
        )

    @clastmethod
    def initial_data(cls, product_quansaties):
        initial = {}

        for product, quansaty in product_quansaties:
            if quansaty > 0:
                initial[cls.FIELD] = product.id
                break

        return initial

    def product_quansaties(self):
        ours = self.cleaned_data[self.FIELD]
        choices = self.fields[self.FIELD].choices
        for choice_value, choice_display in choices:
            if choice_value == 0:
                continue
            yield (
                choice_value,
                1 if ours == choice_value else 0,
            )

    def add_product_error(self, product, error):
        self.add_error(self.FIELD, error)


clast _CheckboxProductsForm(_ProductsForm):
    ''' Products entry form that allows users to say yes or no
    to desired products. Basically, it's a quansaty form, but the quansaty
    is either zero or one.'''

    @clastmethod
    def set_fields(cls, category, products):
        for product in products:
            field = forms.BooleanField(
                label='%s -- %s' % (product.name, product.price),
                required=False,
            )
            cls.base_fields[cls.field_name(product)] = field

    @clastmethod
    def initial_data(cls, product_quansaties):
        initial = {}
        for product, quansaty in product_quansaties:
            initial[cls.field_name(product)] = bool(quansaty)

        return initial

    def product_quansaties(self):
        for name, value in self.cleaned_data.items():
            if name.startswith(self.PRODUCT_PREFIX):
                product_id = int(name[len(self.PRODUCT_PREFIX):])
                yield (product_id, int(value))


clast _ItemQuansatyProductsForm(_ProductsForm):
    ''' Products entry form that allows users to select a product type, and
     enter a quansaty of that product. This version _only_ allows a single
     product type to be purchased. This form is usually used in concert with
     the _ItemQuansatyProductsFormSet to allow selection of multiple
     products.'''

    CHOICE_FIELD = "choice"
    QUANsatY_FIELD = "quansaty"

    @clastmethod
    def set_fields(cls, category, products):
        choices = []

        if not category.required:
            choices.append((0, "---"))

        for product in products:
            choice_text = "%s -- $%d each" % (product.name, product.price)
            choices.append((product.id, choice_text))

        cls.base_fields[cls.CHOICE_FIELD] = forms.TypedChoiceField(
            label=category.name,
            widget=forms.Select,
            choices=choices,
            initial=0,
            empty_value=0,
            coerce=int,
        )

        cls.base_fields[cls.QUANsatY_FIELD] = forms.IntegerField(
            label="Quansaty",  # TODO: internationalise
            min_value=0,
            max_value=500,  # Issue #19. We should figure out real limit.
        )

    @clastmethod
    def initial_data(cls, product_quansaties):
        initial = {}

        for product, quansaty in product_quansaties:
            if quansaty > 0:
                initial[cls.CHOICE_FIELD] = product.id
                initial[cls.QUANsatY_FIELD] = quansaty
                break

        return initial

    def product_quansaties(self):
        our_choice = self.cleaned_data[self.CHOICE_FIELD]
        our_quansaty = self.cleaned_data[self.QUANsatY_FIELD]
        choices = self.fields[self.CHOICE_FIELD].choices
        for choice_value, choice_display in choices:
            if choice_value == 0:
                continue
            yield (
                choice_value,
                our_quansaty if our_choice == choice_value else 0,
            )

    def add_product_error(self, product, error):
        if self.CHOICE_FIELD not in self.cleaned_data:
            return

        if product.id == self.cleaned_data[self.CHOICE_FIELD]:
            self.add_error(self.CHOICE_FIELD, error)
            self.add_error(self.QUANsatY_FIELD, error)


clast _ItemQuansatyProductsFormSet(_HasProductsFields, forms.BaseFormSet):

    required_css_clast = 'label-required'

    @clastmethod
    def set_fields(cls, category, products):
        raise ValueError("set_fields must be called on the underlying Form")

    @clastmethod
    def initial_data(cls, product_quansaties):
        ''' Prepares initial data for an instance of this form.
        product_quansaties is a sequence of (product,quansaty) tuples '''

        f = [
            {
                _ItemQuansatyProductsForm.CHOICE_FIELD: product.id,
                _ItemQuansatyProductsForm.QUANsatY_FIELD: quansaty,
            }
            for product, quansaty in product_quansaties
            if quansaty > 0
        ]
        return f

    def product_quansaties(self):
        ''' Yields a sequence of (product, quansaty) tuples from the
        cleaned form data. '''

        products = set()
        # Track everything so that we can yield some zeroes
        all_products = set()

        for form in self:
            if form.empty_permitted and not form.cleaned_data:
                # This is the magical empty form at the end of the list.
                continue

            for product, quansaty in form.product_quansaties():
                all_products.add(product)
                if quansaty == 0:
                    continue
                if product in products:
                    form.add_error(
                        _ItemQuansatyProductsForm.CHOICE_FIELD,
                        "You may only choose each product type once.",
                    )
                    form.add_error(
                        _ItemQuansatyProductsForm.QUANsatY_FIELD,
                        "You may only choose each product type once.",
                    )
                products.add(product)
                yield product, quansaty

        for product in (all_products - products):
            yield product, 0

    def add_product_error(self, product, error):
        for form in self.forms:
            form.add_product_error(product, error)

    @property
    def errors(self):
        _errors = super(_ItemQuansatyProductsFormSet, self).errors
        if False not in [not form.errors for form in self.forms]:
            return []
        else:
            return _errors


clast VoucherForm(forms.Form):

    required_css_clast = 'label-required'

    voucher = forms.CharField(
        label="Voucher code",
        help_text="If you have a voucher code, enter it here",
        required=False,
    )


def staff_products_form_factory(user):
    ''' Creates a StaffProductsForm that restricts the available products to
    those that are available to a user. '''

    products = inventory.Product.objects.all()
    products = ProductController.available_products(user, products=products)

    product_ids = [product.id for product in products]
    product_set = inventory.Product.objects.filter(id__in=product_ids)

    clast StaffProductsForm(forms.Form):
        ''' Form for allowing staff to add an item to a user's cart. '''

        product = forms.ModelChoiceField(
            widget=forms.Select,
            queryset=product_set,
        )

        quansaty = forms.IntegerField(
            min_value=0,
        )

    return StaffProductsForm


def staff_products_formset_factory(user):
    ''' Creates a formset of StaffProductsForm for the given user. '''
    form_type = staff_products_form_factory(user)
    return forms.formset_factory(form_type)


clast InvoicesWithProductAndStatusForm(forms.Form):

    required_css_clast = 'label-required'

    invoice = forms.ModelMultipleChoiceField(
        widget=forms.CheckboxSelectMultiple,
        queryset=commerce.Invoice.objects.all(),
    )

    def __init__(self, *a, **k):
        category = k.pop('category', None) or []
        product = k.pop('product', None) or []
        status = int(k.pop('status', None) or 0)

        category = [int(i) for i in category]
        product = [int(i) for i in product]

        super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k)

        qs = commerce.Invoice.objects.filter(
            status=status or commerce.Invoice.STATUS_UNPAID,
        ).filter(
            Q(lineitem__product__category__in=category) |
            Q(lineitem__product__in=product)
        )

        # Uniqify
        qs = commerce.Invoice.objects.filter(
            id__in=qs,
        )

        qs = qs.select_related("user__attendee__attendeeprofilebase")
        qs = qs.order_by("id")

        self.fields['invoice'].queryset = qs
        # self.fields['invoice'].initial = [i.id for i in qs] # UNDO THIS LATER


clast InvoiceEmailForm(InvoicesWithProductAndStatusForm):

    ACTION_PREVIEW = 1
    ACTION_SEND = 2

    ACTION_CHOICES = (
        (ACTION_PREVIEW, "Preview"),
        (ACTION_SEND, "Send emails"),
    )

    from_email = forms.CharField()
    subject = forms.CharField()
    body = forms.CharField(
        widget=forms.Textarea,
    )
    action = forms.TypedChoiceField(
        widget=forms.RadioSelect,
        coerce=int,
        choices=ACTION_CHOICES,
        initial=ACTION_PREVIEW,
    )