Changeset View
Changeset View
Standalone View
Standalone View
looper/forms.py
| import logging | import logging | ||||
| import typing | from typing import Optional | ||||
| from django import forms | from django import forms | ||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||
| import django.contrib.auth.forms as auth_forms | import django.contrib.auth.forms as auth_forms | ||||
| from . import exceptions, models, model_mixins, money, form_fields | from . import exceptions, models, model_mixins, money, form_fields | ||||
| Show All 23 Lines | def __getitem__(self, field_name: str): | ||||
| # `bound_field.css_classes()` is used by BaseForm._html_output() to obtain the | # `bound_field.css_classes()` is used by BaseForm._html_output() to obtain the | ||||
| # CSS classes that are used by the <li> element (when using form.as_ul()). | # CSS classes that are used by the <li> element (when using form.as_ul()). | ||||
| # Fortunately it accepts extra CSS classes, but unfortunately we cannot pass | # Fortunately it accepts extra CSS classes, but unfortunately we cannot pass | ||||
| # anything to the call. Instead, we replace that function with our own, and | # anything to the call. Instead, we replace that function with our own, and | ||||
| # inject the extra classes there. | # inject the extra classes there. | ||||
| orig_css_classes = bound_field.css_classes | orig_css_classes = bound_field.css_classes | ||||
| def css_classes(extra_class: typing.Optional[str] = None) -> str: | def css_classes(extra_class: Optional[str] = None) -> str: | ||||
| widget_classes: str = bound_field.field.widget.attrs.get('class', '') | widget_classes: str = bound_field.field.widget.attrs.get('class', '') | ||||
| if extra_class: | if extra_class: | ||||
| widget_classes = f'{widget_classes} {extra_class}' | widget_classes = f'{widget_classes} {extra_class}' | ||||
| return orig_css_classes(widget_classes) | return orig_css_classes(widget_classes) | ||||
| bound_field.css_classes = css_classes | bound_field.css_classes = css_classes | ||||
| return bound_field | return bound_field | ||||
| Show All 25 Lines | class BaseCheckoutForm(BasePaymentForm): | ||||
| # The CheckoutView will test whether the user has recently already created a subscription. | # The CheckoutView will test whether the user has recently already created a subscription. | ||||
| # If that is the case, the user will get a warning about this, and will have to confirm | # If that is the case, the user will get a warning about this, and will have to confirm | ||||
| # the creation of a new subscription. | # the creation of a new subscription. | ||||
| approve_new_subscription = forms.BooleanField( | approve_new_subscription = forms.BooleanField( | ||||
| label='Another Membership', | label='Another Membership', | ||||
| required=False, | required=False, | ||||
| initial=False, | initial=False, | ||||
| widget=forms.HiddenInput(), | widget=forms.HiddenInput(), | ||||
| help_text='') | help_text='', | ||||
| ) | |||||
| # These are used when a payment fails, so that the next attempt to pay can reuse | # These are used when a payment fails, so that the next attempt to pay can reuse | ||||
| # the already-created subscription and order. | # the already-created subscription and order. | ||||
| subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False) | subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False) | ||||
| order_pk = forms.CharField(widget=forms.HiddenInput(), required=False) | order_pk = forms.CharField(widget=forms.HiddenInput(), required=False) | ||||
| def show_approve_new_subscription(self, help_text: str): | def show_approve_new_subscription(self, help_text: str): | ||||
| """Show the 'approve_new_subscription' checkbox.""" | """Show the 'approve_new_subscription' checkbox.""" | ||||
| Show All 36 Lines | def clean_status(self): | ||||
| old_status = self.instance.status | old_status = self.instance.status | ||||
| new_status = self.cleaned_data['status'] | new_status = self.cleaned_data['status'] | ||||
| if old_status == new_status: | if old_status == new_status: | ||||
| # Always accept the current status. | # Always accept the current status. | ||||
| return new_status | return new_status | ||||
| if not self.instance.may_transition_to(new_status): | if not self.instance.may_transition_to(new_status): | ||||
| raise ValidationError(f'Status transition {old_status.title()} → ' | raise ValidationError( | ||||
| f'{new_status.title()} is not allowed.') | f'Status transition {old_status.title()} → {new_status.title()} is not allowed.' | ||||
| ) | |||||
| return new_status | return new_status | ||||
| class PaymentMethodLimitMixin(forms.ModelForm): | class PaymentMethodLimitMixin(forms.ModelForm): | ||||
| def __init__(self, *args, instance: Optional[models.Subscription] = None, **kwargs) -> None: | |||||
| def __init__(self, *args, instance: typing.Optional[models.Subscription] = None, **kwargs) \ | |||||
| -> None: | |||||
| if not instance: | if not instance: | ||||
| # When there is no instance, we don't know whose payment methods to list, | # When there is no instance, we don't know whose payment methods to list, | ||||
| # so it's better to not list them at all. | # so it's better to not list them at all. | ||||
| invalid_user_id = -1 | invalid_user_id = -1 | ||||
| queryset = models.PaymentMethod.objects.filter(user_id=invalid_user_id) | queryset = models.PaymentMethod.objects.filter(user_id=invalid_user_id) | ||||
| else: | else: | ||||
| queryset = instance.user.paymentmethod_set | queryset = instance.user.paymentmethod_set | ||||
| self.base_fields['payment_method'].queryset = queryset | self.base_fields['payment_method'].queryset = queryset | ||||
| super().__init__(*args, instance=instance, **kwargs) | super().__init__(*args, instance=instance, **kwargs) | ||||
| class SubscriptionAdminForm(StateMachineMixin, PaymentMethodLimitMixin, forms.ModelForm): | class SubscriptionAdminForm(StateMachineMixin, PaymentMethodLimitMixin, forms.ModelForm): | ||||
| class Meta: | class Meta: | ||||
| model = models.Subscription | model = models.Subscription | ||||
| exclude = ['id'] | exclude = ['id'] | ||||
| def clean_collection_method(self) -> str: | def clean_collection_method(self) -> str: | ||||
| payment_method = self.cleaned_data['payment_method'] | payment_method = self.cleaned_data['payment_method'] | ||||
| collection_method = self.cleaned_data['collection_method'] | collection_method = self.cleaned_data['collection_method'] | ||||
| if self.instance.collection_method != collection_method and \ | if self.instance.collection_method != collection_method and 'managed' in { | ||||
| 'managed' in {self.instance.collection_method, collection_method}: | self.instance.collection_method, | ||||
| raise ValidationError('Toggling between managed and non-managed is not ' | collection_method, | ||||
| 'possible at the moment.') | }: | ||||
| raise ValidationError( | |||||
| 'Toggling between managed and non-managed is not possible at the moment.' | |||||
| ) | |||||
| if collection_method != 'automatic': | if collection_method != 'automatic': | ||||
| return collection_method | return collection_method | ||||
| if not payment_method: | if not payment_method: | ||||
| raise ValidationError('Automatic payment requires a payment method.') | raise ValidationError('Automatic payment requires a payment method.') | ||||
| return collection_method | return collection_method | ||||
| class OrderAdminForm(StateMachineMixin, PaymentMethodLimitMixin, forms.ModelForm): | class OrderAdminForm(StateMachineMixin, PaymentMethodLimitMixin, forms.ModelForm): | ||||
| class Meta: | class Meta: | ||||
| model = models.Order | model = models.Order | ||||
| exclude = ['id'] | exclude = ['id'] | ||||
| class AdminTransactionRefundForm(forms.ModelForm): | class AdminTransactionRefundForm(forms.ModelForm): | ||||
| class Meta: | class Meta: | ||||
| model = models.Order | model = models.Order | ||||
| exclude = ['id'] | exclude = ['id'] | ||||
| refund_amount = form_fields.MoneyFormField( | refund_amount = form_fields.MoneyFormField(required=False, help_text='Amount to refund.') | ||||
| required=False, | |||||
| help_text='Amount to refund.') | |||||
| log = log.getChild('AdminTransactionRefundForm') | log = log.getChild('AdminTransactionRefundForm') | ||||
| def clean_refund_amount(self): | def clean_refund_amount(self): | ||||
| refund_in_cents = self.cleaned_data['refund_amount'] | refund_in_cents = self.cleaned_data['refund_amount'] | ||||
| refund = money.Money(self.instance.currency, refund_in_cents) | refund = money.Money(self.instance.currency, refund_in_cents) | ||||
| if not refund: | if not refund: | ||||
| # No refund is fine. | # No refund is fine. | ||||
| return | return | ||||
| refundable = self.instance.refundable | refundable = self.instance.refundable | ||||
| if refund > refundable: | if refund > refundable: | ||||
| raise ValidationError( | raise ValidationError(f'Cannot refund more than {refundable.with_currency_symbol()}') | ||||
| f'Cannot refund more than {refundable.with_currency_symbol()}') | |||||
| if refund.cents < 0: | if refund.cents < 0: | ||||
| raise ValidationError(f'Cannot refund negative amounts') | raise ValidationError(f'Cannot refund negative amounts') | ||||
| return refund | return refund | ||||
| def save(self, commit=True): | def save(self, commit=True): | ||||
| """Save the model and process the refund if successful.""" | """Save the model and process the refund if successful.""" | ||||
| instance: models.Transaction = super().save(commit=commit) | instance: models.Transaction = super().save(commit=commit) | ||||
| refund: money.Money = self.cleaned_data['refund_amount'] | refund: money.Money = self.cleaned_data['refund_amount'] | ||||
| if not refund: | if not refund: | ||||
| self.log.debug('Not processing empty refund for transaction pk=%r', self.instance.pk) | self.log.debug('Not processing empty refund for transaction pk=%r', self.instance.pk) | ||||
| return instance | return instance | ||||
| self.log.debug('Processing refund of %s for transaction pk=%r', refund, self.instance.pk) | self.log.debug('Processing refund of %s for transaction pk=%r', refund, self.instance.pk) | ||||
| try: | try: | ||||
| instance.refund(refund) | instance.refund(refund) | ||||
| except exceptions.GatewayError as ex: | except exceptions.GatewayError as ex: | ||||
| self.log.exception('Error refunding %s for transaction pk=%r: %s', | self.log.exception( | ||||
| refund, self.instance.pk, ex) | 'Error refunding %s for transaction pk=%r: %s', refund, self.instance.pk, ex | ||||
| ) | |||||
| raise ValidationError(f'The refund could not be processed: {ex}') | raise ValidationError(f'The refund could not be processed: {ex}') | ||||
| return instance | return instance | ||||
| class EmailRequiredMixin: | class EmailRequiredMixin: | ||||
| cleaned_data: dict | cleaned_data: dict | ||||
| instance: User | instance: User | ||||
| def clean_email(self) -> str: | def clean_email(self) -> str: | ||||
| if self.cleaned_data['email']: | if self.cleaned_data['email']: | ||||
| return self.cleaned_data['email'] | return self.cleaned_data['email'] | ||||
| try: | try: | ||||
| if self.instance.customer and self.instance.customer.billing_email: | if self.instance.customer and self.instance.customer.billing_email: | ||||
| # If the user has no email but it does have a billing email, | # If the user has no email but it does have a billing email, | ||||
| # just use that instead. | # just use that instead. | ||||
| return self.instance.customer.billing_email | return self.instance.customer.billing_email | ||||
| except models.Customer.DoesNotExist: | except models.Customer.DoesNotExist: | ||||
| pass | pass | ||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||
| raise ValidationError('Every user should have an email address') | raise ValidationError('Every user should have an email address') | ||||
| class AdminUserChangeForm(EmailRequiredMixin, auth_forms.UserChangeForm): | class AdminUserChangeForm(EmailRequiredMixin, auth_forms.UserChangeForm): | ||||
| pass | pass | ||||
| class AdminUserCreationForm(EmailRequiredMixin, auth_forms.UserCreationForm): | class AdminUserCreationForm(EmailRequiredMixin, auth_forms.UserCreationForm): | ||||
| pass | pass | ||||