Source code for salesman.conf

from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Any, Callable

from django.utils.functional import cached_property

if TYPE_CHECKING:  # pragma: no cover
    from django.http import HttpRequest
    from rest_framework.serializers import Serializer

    from salesman.basket.modifiers import BasketModifier
    from salesman.checkout.payment import PaymentMethod
    from salesman.orders.status import BaseOrderStatus


[docs]class AppSettings: @cached_property def SALESMAN_PRODUCT_TYPES(self) -> dict[str, type[Serializer]]: """ A dictionary of product types and their respected serializers that are availible for purchase as product. Should be formated as ``'app_label.Model': 'path.to.Serializer'``. """ from salesman.core.typing import Product product_types = self._setting("SALESMAN_PRODUCT_TYPES", {}) ret = {} for key, value in product_types.items(): model = self._model(key) ret[key] = self._class(value) if not isinstance(model, Product): self._error( f"Product type `{key}` must subclass `django.db.models.Model` and " f"implement the `salesman.core.typing.Product` protocol. " f"Required fields: `id`, `name`, `code`. " f"Required methods: `get_price(self, request)`. " ) return ret @cached_property def SALESMAN_BASKET_MODIFIERS(self) -> list[type[BasketModifier]]: """ A list of strings formated as ``path.to.CustomModifier``. Modifiers must extend ``salesman.basket.modifiers.BasketModifier`` class. and define a unique ``identifier`` attribute. """ from salesman.basket.modifiers import BasketModifier basket_modifiers = self._setting("SALESMAN_BASKET_MODIFIERS", []) ret, identifiers = [], [] for value in basket_modifiers: modifier: type[BasketModifier] = self._class(value) identifier = getattr(modifier, "identifier", None) if not issubclass(modifier, BasketModifier): self._error(f"Modifer `{modifier}` must subclass `{BasketModifier}`.") if not identifier: self._error(f"Modifier `{modifier}` must define a unique `idetifier`.") if identifier in identifiers: self._error(f"Modifier `{identifier}` appears more than once.") identifiers.append(identifier) ret.append(modifier) return ret @cached_property def SALESMAN_BASKET_ITEM_VALIDATOR(self) -> Callable[..., dict[str, Any]]: """ A dotted path to basket item validator function. """ default = "salesman.basket.utils.validate_basket_item" value = self._setting("SALESMAN_BASKET_ITEM_VALIDATOR", default) return self._function(value) @property def SALESMAN_BASKET_MODEL(self) -> str: """ A dotted path to the Basket model. Must be set before migrations. """ value: str = self._setting("SALESMAN_BASKET_MODEL", "salesmanbasket.Basket") self._model_label(value) return value @property def SALESMAN_BASKET_ITEM_MODEL(self) -> str: """ A dotted path to the Basket Item model. Must be set before migrations. """ value: str = self._setting( "SALESMAN_BASKET_ITEM_MODEL", "salesmanbasket.BasketItem", ) self._model_label(value) return value @cached_property def SALESMAN_PAYMENT_METHODS(self) -> list[type[PaymentMethod]]: """ A list of strings formated as ``path.to.CustomPayment``. Payments must extend ``salesman.checkout.payment.PaymentMethod`` class and define a unique ``identifier`` attribute. """ from salesman.checkout.payment import PaymentMethod payment_methods = self._setting("SALESMAN_PAYMENT_METHODS", []) ret, identifiers = [], [] for value in payment_methods: payment = self._class(value) identifier = getattr(payment, "identifier", None) if not issubclass(payment, PaymentMethod): self._error(f"Payment `{payment}` must subclass `{PaymentMethod}`.") if not getattr(payment, "label", None): self._error(f"Payment `{payment}` must define a `label`.") if not identifier: self._error(f"Payment `{payment}` must define a unique `identifier`.") if identifier in identifiers: self._error(f"Payment `{identifier}` appears more than once.") identifiers.append(identifier) ret.append(payment) return ret @cached_property def SALESMAN_ORDER_STATUS(self) -> type[BaseOrderStatus]: """ A dotted path to enum class that contains available order statuses. Overriden class must extend ``salesman.orders.status.BaseOrderStatus`` class. Can optionally override a class method ``get_payable`` that returns a list of statuses an order is eligible to be paid from, ``get_transitions`` method that returns a dict of statuses with their transitions and ``validate_transition`` method to validate status transitions. """ from salesman.orders.status import BaseOrderStatus default = "salesman.orders.status.OrderStatus" value = self._setting("SALESMAN_ORDER_STATUS", default) status: type[BaseOrderStatus] = self._class(value) if not issubclass(status, BaseOrderStatus): self._error(f"Status `{status}` must subclass `{BaseOrderStatus}`.") required = ["NEW", "CREATED", "COMPLETED", "REFUNDED"] for item in required: if item not in status.names or status[item].value != item: # type: ignore self._error( "Status must specify members with names/values " "`NEW`, `CREATED`, `COMPLETED` and `REFUNDED`." ) return status @cached_property def SALESMAN_ORDER_REFERENCE_GENERATOR(self) -> Callable[[HttpRequest], str]: """ A dotted path to reference generator function for new orders. Function should accept a django request object as param: ``request``. """ default = "salesman.orders.utils.generate_ref" value = self._setting("SALESMAN_ORDER_REFERENCE_GENERATOR", default) return self._function(value) @cached_property def SALESMAN_ORDER_SERIALIZER(self) -> type[Serializer]: """ A dotted path to a serializer class for Order. """ default = "salesman.orders.serializers.OrderSerializer" value = self._setting("SALESMAN_ORDER_SERIALIZER", default) serializer: type[Serializer] = self._class(value) return serializer @cached_property def SALESMAN_ORDER_SUMMARY_SERIALIZER(self) -> type[Serializer]: """ A dotted path to a summary serializer class for Order. """ value = self._setting("SALESMAN_ORDER_SUMMARY_SERIALIZER", None) if not value: return self.SALESMAN_ORDER_SERIALIZER serializer: type[Serializer] = self._class(value) return serializer @property def SALESMAN_ORDER_MODEL(self) -> str: """ A dotted path to the Order model. Must be set before migrations. """ value: str = self._setting("SALESMAN_ORDER_MODEL", "salesmanorders.Order") self._model_label(value) return value @property def SALESMAN_ORDER_ITEM_MODEL(self) -> str: """ A dotted path to the Order Item model. Must be set before migrations. """ value: str = self._setting( "SALESMAN_ORDER_ITEM_MODEL", "salesmanorders.OrderItem", ) self._model_label(value) return value @property def SALESMAN_ORDER_PAYMENT_MODEL(self) -> str: """ A dotted path to the Order Payment model. Must be set before migrations. """ value: str = self._setting( "SALESMAN_ORDER_PAYMENT_MODEL", "salesmanorders.OrderPayment", ) self._model_label(value) return value @property def SALESMAN_ORDER_NOTE_MODEL(self) -> str: """ A dotted path to the Order Note model. Must be set before migrations. """ value: str = self._setting( "SALESMAN_ORDER_NOTE_MODEL", "salesmanorders.OrderNote", ) self._model_label(value) return value @cached_property def SALESMAN_PRICE_FORMATTER(self) -> Callable[..., str]: """ A dotted path to price formatter function. Function should accept a value of type: ``Decimal`` and return a price formatted string. Also recieves a ``context`` dictionary with additional render data like ``request`` and either the ``basket`` or ``order`` object. """ default = "salesman.core.utils.format_price" value = self._setting("SALESMAN_PRICE_FORMATTER", default) return self._function(value) @cached_property def SALESMAN_ADDRESS_VALIDATOR(self) -> Callable[..., str]: """ A dotted path to address validator function. Function should accept a string value and return a validated version. Also recieves a ``context`` dictionary with additional validator context data like ``request``, a ``basket`` object and ``address`` type (set to either *shipping* or *billing*). """ default = "salesman.checkout.utils.validate_address" value = self._setting("SALESMAN_ADDRESS_VALIDATOR", default) return self._function(value) @cached_property def SALESMAN_EXTRA_VALIDATOR(self) -> Callable[..., dict[str, Any]]: """ A dotted path to extra validator function. Function should accept a dict value and return a validated version. Also recieves a ``context`` dictionary with additional validator context data like ``request``, a ``basket`` object and ``basket_item`` in case validation is for bakset item. """ default = "salesman.basket.utils.validate_extra" value = self._setting("SALESMAN_EXTRA_VALIDATOR", default) return self._function(value) @property def SALESMAN_ADMIN_REGISTER(self) -> bool: """ Set to ``False`` to skip Salesman admin registration, in case you wish to build your own ``ModelAdmin`` for Django or Wagtail. """ value: bool = self._setting("SALESMAN_ADMIN_REGISTER", True) return value @cached_property def SALESMAN_ADMIN_JSON_FORMATTER(self) -> Callable[..., str]: """ A dotted path to JSON formatter function. Function should accept a dict value and return an HTML formatted string. Also recieves a ``context`` dictionary with additional render data. """ default = "salesman.admin.utils.format_json" value = self._setting("SALESMAN_ADMIN_JSON_FORMATTER", default) return self._function(value) @property def SALESMAN_ALLOW_ANONYMOUS_USER_CHECKOUT(self) -> bool: """ Set to ``False`` if you want to prevent to order if user not authorized. """ value: bool = self._setting("SALESMAN_ALLOW_ANONYMOUS_USER_CHECKOUT", True) return value def _setting(self, name: str, default: Any = None) -> Any: from django.conf import settings return getattr(settings, name, default) def _error(self, message: str | Exception) -> None: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured(message) def _model_label(self, value: str) -> tuple[str, str]: try: app_label, model_name = value.split(".") except ValueError: self._error(f"Invalid model `{value}`, format as `app_label.model_name`.") return app_label, model_name def _model(self, label: str) -> Any: from django.apps import apps app_label, model_name = self._model_label(label) try: return apps.get_model(app_label, model_name) except (LookupError, ValueError) as e: self._error(e) def _class(self, path: str) -> Any: value = self._import(path) if not inspect.isclass(value): self._error(f"Specified `{value}` is not a class.") return value def _function(self, path: str) -> Callable[..., Any]: value: Callable[..., Any] = self._import(path) if not inspect.isfunction(value): self._error(f"Specified `{value}` is not a function.") return value def _import(self, path: str) -> Any: from django.utils.module_loading import import_string try: return import_string(path) except ImportError as e: self._error(e)
app_settings = AppSettings()