Source code for polaris.integrations.forms

from django import forms
from django.utils.formats import localize
from django.utils.translation import gettext_lazy as _
from django.forms.widgets import TextInput

from polaris.models import Transaction, Asset


class CardNumberInput(TextInput):
    template_name = "widgets/card_number.html"


class CardExpirationInput(TextInput):
    template_name = "widgets/card_expiration.html"


class CardCvvInput(TextInput):
    template_name = "widgets/card_cvv.html"


class CreditCardField(forms.CharField):
    def __init__(self, placeholder=None, *args, **kwargs):
        super().__init__(
            # override default widget
            widget=CardNumberInput(attrs={"placeholder": placeholder}),
            *args,
            **kwargs,
        )

    default_error_messages = {
        "invalid": _("The credit card number is invalid"),
    }

    @staticmethod
    def luhn_checksum(card_number):
        def digits_of(n):
            return [int(d) for d in str(n)]

        digits = digits_of(card_number)
        odd_digits = digits[-1::-2]
        even_digits = digits[-2::-2]
        checksum = 0
        checksum += sum(odd_digits)
        for ed in even_digits:
            checksum += sum(digits_of(ed * 2))
        return checksum % 10

    def is_luhn_valid(self, card_number):
        return self.luhn_checksum(card_number) == 0

    def clean(self, value):
        # ensure no spaces or dashes
        value = value.replace(" ", "").replace("-", "")
        if not (value.isdigit() and self.is_luhn_valid(value)):
            raise forms.ValidationError(self.error_messages["invalid"])
        return value


class CreditCardForm(forms.Form):
    """
    A generic form for collecting credit or debit card information.

    Ensures `card_number` is valid, but does not validate the `expiration` or
    `cvv`. Subclass this form for additional validation.
    """

    name = forms.CharField(label=_("Name"))
    card_number = CreditCardField(label=_("Card Number"))
    expiration = forms.Field(widget=CardExpirationInput, label=_("Expiration"))
    cvv = forms.Field(widget=CardCvvInput, label=_("CVV"))


[docs]class TransactionForm(forms.Form): """ A base class for collecting transaction information. Developers must define subclasses to collect additional information and apply additional validation. This form assumes the amount collected is in units of a Stellar :class:`~polaris.models.Asset`. If the amount of an :class:`~polaris.models.OffChainAsset` must be collected, create a different form. Note that Polaris' base UI treats the amount field on this form and its subclasses differently than other forms. Specifically, Polaris automatically adds the asset's symbol to the input field, adds a placeholder value of 0, makes the fee table visible (by default), and uses the amount entered to update the fee table on each change. If you do not want the fee table to be displayed when this form class is rendered, set ``"show_fee_table"`` to ``False`` in the dict returned from :meth:`~polaris.integrations.DepositIntegration.content_for_template`. Fee calculation within the UI is done using the asset's fixed and percentage fee values saved to the database. If those values are not present, Polaris makes calls to the anchor's `/fee` endpoint and displays the response value to the user. If your `/fee` endpoint requires a `type` parameter, add a ``TransactionForm.type`` attribute to the form. Polaris will detect the attribute's presence on the form and include it in `/fee` requests. The :attr:`amount` field is validated with the :meth:`clean_amount` function, which ensures the amount is within the bounds for the asset type. """ def __init__(self, transaction: Transaction, *args, **kwargs): super().__init__(*args, **kwargs) self.transaction = transaction self.asset = transaction.asset self.decimal_places = self.asset.significant_decimals if transaction.kind == Transaction.KIND.deposit: self.min_amount = round(self.asset.deposit_min_amount, self.decimal_places) self.max_amount = round(self.asset.deposit_max_amount, self.decimal_places) self.min_default = ( getattr(Asset, "_meta").get_field("deposit_min_amount").default ) self.max_default = ( getattr(Asset, "_meta").get_field("deposit_max_amount").default ) else: self.min_amount = round( self.asset.withdrawal_min_amount, self.decimal_places ) self.max_amount = round( self.asset.withdrawal_max_amount, self.decimal_places ) self.min_default = ( getattr(Asset, "_meta").get_field("withdrawal_min_amount").default ) self.max_default = ( getattr(Asset, "_meta").get_field("withdrawal_max_amount").default ) # Re-initialize the 'amount' field now that we have all the parameters necessary self.fields["amount"].__init__( widget=forms.TextInput( attrs={ "class": "polaris-transaction-form-amount", "inputmode": "decimal", "symbol": self.asset.symbol, } ), min_value=self.min_amount, max_value=self.max_amount, decimal_places=self.decimal_places, label=_("Amount"), localize=True, ) limit_str = "" if self.min_amount > self.min_default and self.max_amount < self.max_default: limit_str = f"({localize(self.min_amount)} - {localize(self.max_amount)})" elif self.min_amount > self.min_default: limit_str = _("(minimum: %s)") % localize(self.min_amount) elif self.max_amount < self.max_default: limit_str = _("(maximum: %s)") % localize(self.max_amount) if limit_str: self.fields["amount"].label += " " + limit_str amount = forms.DecimalField() def clean_amount(self): """Validate the provided amount of an asset.""" amount = round(self.cleaned_data["amount"], self.decimal_places) if amount < self.min_amount: raise forms.ValidationError( _("The minimum amount is: %s") % localize(round(self.min_amount, self.decimal_places)) ) elif amount > self.max_amount: raise forms.ValidationError( _("The maximum amount is: %s") % localize(round(self.max_amount, self.decimal_places)) ) return amount