Changeset View
Changeset View
Standalone View
Standalone View
looper/admin.py
| import copy | import copy | ||||
| from typing import ( | from typing import ( | ||||
| Any, | Any, | ||||
| Callable, | Callable, | ||||
| Dict, | Dict, | ||||
| List, | List, | ||||
| Optional, | Optional, | ||||
| Sequence, | Sequence, | ||||
| Set, | Set, | ||||
| Tuple, | Tuple, | ||||
| Union, | Union, | ||||
| ) | ) | ||||
| import django.db.models | |||||
| from django.contrib import admin | from django.contrib import admin | ||||
| from django.db.models.query import QuerySet | |||||
| from django.http import HttpRequest | |||||
| from django.urls import reverse | |||||
| from django.utils.html import format_html | from django.utils.html import format_html | ||||
| from django.utils.safestring import SafeText | from django.utils.safestring import SafeText | ||||
| from django.urls import reverse | import django.db.models | ||||
| from looper import forms, models, decorators | from looper import forms, models, decorators | ||||
| from looper.utils import make_absolute_url | from looper.utils import make_absolute_url | ||||
| USER_SEARCH_FIELDS = ( | USER_SEARCH_FIELDS = ( | ||||
| 'user__email', | 'user__email', | ||||
| 'user__customer__billing_email', | 'user__customer__billing_email', | ||||
| 'user__customer__full_name', | 'user__customer__full_name', | ||||
| 'user__username', | 'user__username', | ||||
| ) | ) | ||||
| def interval(subscription: models.Subscription) -> str: | def interval(subscription: models.Subscription) -> str: | ||||
| """Combines interval_unit and interval_length into one string. | """Combines interval_unit and interval_length into one string. | ||||
| This helps making the admin's list_display a little bit tidier. | This helps making the admin's list_display a little bit tidier. | ||||
| """ | """ | ||||
| if subscription.interval_length == 1: | if subscription.interval_length == 1: | ||||
| return subscription.interval_unit | return subscription.interval_unit | ||||
| return f'{subscription.interval_length} {subscription.interval_unit}' | return f'{subscription.interval_length} {subscription.interval_unit}' | ||||
| @decorators.short_description('ID') | @decorators.short_description('ID') | ||||
| def id_column(instance) -> str: | def id_column(instance: django.db.models.Model) -> str: | ||||
| pk = instance.pk | pk = instance.pk | ||||
| if pk is None: | if pk is None: | ||||
| return '-' | return '-' | ||||
| if isinstance(pk, int): | if isinstance(pk, int): | ||||
| return f'#{pk}' | return f'#{pk}' | ||||
| return str(pk) | return str(pk) | ||||
| ▲ Show 20 Lines • Show All 60 Lines • ▼ Show 20 Lines | def create_admin_fk_link(field_name: str, short_description: str, view_name: str) -> LinkFunc: | ||||
| :param view_name: The admin view to link to. Must take one parameter `object_id`. | :param view_name: The admin view to link to. Must take one parameter `object_id`. | ||||
| For example, to create a link to a customer (instead of a drop-down to edit it), | For example, to create a link to a customer (instead of a drop-down to edit it), | ||||
| use: | use: | ||||
| `customer_link = create_admin_fk_link('customer', 'Customer', 'admin:looper_customer_change')` | `customer_link = create_admin_fk_link('customer', 'Customer', 'admin:looper_customer_change')` | ||||
| """ | """ | ||||
| def create_link(model_instance) -> str: | def create_link(model_instance: django.db.models.Model) -> str: | ||||
| referenced = getattr(model_instance, field_name) | referenced: Optional[object] = getattr(model_instance, field_name) | ||||
| if referenced is None: | if referenced is None: | ||||
| return '-' | return '-' | ||||
| assert isinstance( | assert isinstance( | ||||
| referenced, django.db.models.Model | referenced, django.db.models.Model | ||||
| ), f'Expected Model, not {type(referenced)}' | ), f'Expected Model, not {type(referenced)}' | ||||
| admin_link = reverse(view_name, kwargs={'object_id': referenced.pk}) | admin_link = reverse(view_name, kwargs={'object_id': referenced.pk}) | ||||
| return format_html('<a href="{}">{}</a>', admin_link, str(referenced)) | return format_html('<a href="{}">{}</a>', admin_link, str(referenced)) | ||||
| Show All 28 Lines | |||||
| class PlanAdmin(admin.ModelAdmin): | class PlanAdmin(admin.ModelAdmin): | ||||
| list_display = [id_column, 'name', 'is_active', product_link] | list_display = [id_column, 'name', 'is_active', product_link] | ||||
| list_display_links = [id_column, 'name', 'is_active'] | list_display_links = [id_column, 'name', 'is_active'] | ||||
| inlines = [ | inlines = [ | ||||
| PlanVariationInline, | PlanVariationInline, | ||||
| ] | ] | ||||
| def copy_to_clipboard_link(view_name: str, kwargs: dict, link_text: str, title: str) -> SafeText: | def copy_to_clipboard_link( | ||||
| view_name: str, kwargs: Dict[str, Any], link_text: str, title: str | |||||
| ) -> SafeText: | |||||
| """Construct a link that copies a link to the clipboard when clicked. | """Construct a link that copies a link to the clipboard when clicked. | ||||
| This is for things like payment links, e.g. links that should be sent to | This is for things like payment links, e.g. links that should be sent to | ||||
| a customer (instead of followed by the admin). | a customer (instead of followed by the admin). | ||||
| """ | """ | ||||
| link = reverse(view_name, kwargs=kwargs) | link = reverse(view_name, kwargs=kwargs) | ||||
| link = make_absolute_url(link) | link = make_absolute_url(link) | ||||
| ▲ Show 20 Lines • Show All 41 Lines • ▼ Show 20 Lines | class OrderInline(admin.TabularInline): | ||||
| can_delete = False | can_delete = False | ||||
| FieldsetType = Sequence[Tuple[Optional[str], Dict[str, Any]]] | FieldsetType = Sequence[Tuple[Optional[str], Dict[str, Any]]] | ||||
| class EditableWhenNewMixin: | class EditableWhenNewMixin: | ||||
| # These should be set on the subclass using this mix-in. | # These should be set on the subclass using this mix-in. | ||||
| readonly_fields: Sequence[Union[str, Callable]] | readonly_fields: Sequence[Union[str, Callable[..., object]]] | ||||
| fieldsets: FieldsetType | fieldsets: FieldsetType | ||||
| # Those callable fields will become editable when adding a new instance. | # Those callable fields will become editable when adding a new instance. | ||||
| # Assumes that these callables have a 'admin_order_field' property that | # Assumes that these callables have a 'admin_order_field' property that | ||||
| # contains the field they map to. | # contains the field they map to. | ||||
| editable_when_new: Set[Callable] = set() | editable_when_new: Set[Callable[..., object]] = set() | ||||
| def get_readonly_fields(self, request, obj=None): | def get_readonly_fields( | ||||
| self, request: HttpRequest, obj: Optional[django.db.models.Model] = None | |||||
| ) -> Any: | |||||
| if obj: # Editing an existing object | if obj: # Editing an existing object | ||||
| return self.readonly_fields | return self.readonly_fields | ||||
| # When adding a new subscription, remove the the 'xxx_link' callables | # When adding a new subscription, remove the the 'xxx_link' callables | ||||
| # from the readonly_fields list. This optimises for the more common | # from the readonly_fields list. This optimises for the more common | ||||
| # case of subscriptions being created by the checkout process, rather | # case of subscriptions being created by the checkout process, rather | ||||
| # than manually via the admin. | # than manually via the admin. | ||||
| return [field for field in self.readonly_fields if not hasattr(field, '__call__')] | return [field for field in self.readonly_fields if not hasattr(field, '__call__')] | ||||
| def get_fieldsets(self, request, obj=None) -> FieldsetType: | def get_fieldsets( | ||||
| self, request: HttpRequest, obj: Optional[django.db.models.Model] = None | |||||
| ) -> FieldsetType: | |||||
| if obj: # Editing an existing object. | if obj: # Editing an existing object. | ||||
| return self.fieldsets | return self.fieldsets | ||||
| fieldsets = copy.deepcopy(self.fieldsets) | fieldsets = copy.deepcopy(self.fieldsets) | ||||
| for fieldset in fieldsets: | for fieldset in fieldsets: | ||||
| label, config = fieldset | label, config = fieldset | ||||
| if 'fields' not in config: | if 'fields' not in config: | ||||
| ▲ Show 20 Lines • Show All 127 Lines • ▼ Show 20 Lines | fields = [ | ||||
| 'amount_refunded', | 'amount_refunded', | ||||
| 'refunded_at', | 'refunded_at', | ||||
| refund_button_link, | refund_button_link, | ||||
| ] | ] | ||||
| readonly_fields = fields | readonly_fields = fields | ||||
| can_delete = False | can_delete = False | ||||
| def mark_as_paid(modeladmin, request, queryset): | def mark_as_paid( | ||||
| modeladmin: 'OrderAdmin', request: HttpRequest, queryset: 'QuerySet[models.Order]' | |||||
| ) -> None: | |||||
| """Mark all selected orders as 'paid'.""" | """Mark all selected orders as 'paid'.""" | ||||
| for order in queryset: | for order in queryset: | ||||
| order.status = 'paid' | order.status = 'paid' | ||||
| order.save() | order.save() | ||||
| @decorators.short_description('Receipt') | @decorators.short_description('Receipt') | ||||
| def order_receipt_link(order: models.Order) -> str: | def order_receipt_link(order: models.Order) -> str: | ||||
| ▲ Show 20 Lines • Show All 198 Lines • Show Last 20 Lines | |||||