Aller au contenu

Views

EbouticBasketForm = forms.formset_factory(BasketItemForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1) module-attribute

CanViewMixin

Bases: GenericContentPermissionMixinBuilder

Ensure the user has permission to view this view's object.

Raises:

Type Description
PermissionDenied

if the user cannot edit this view's object.

IsSubscriberMixin

Bases: AccessMixin

Check if the user is a subscriber.

Raises:

Type Description
PermissionDenied

if the user isn't subscribed.

FragmentMixin

Bases: TemplateResponseMixin, ContextMixin

Make a view buildable as a fragment that can be embedded in a template.

Most fragments are used in two different ways : - in the request/response cycle, like any regular view - in templates, where the rendering is done in another view

This mixin aims to simplify the initial fragment rendering. The rendered fragment will then be able to re-render itself through the request/response cycle if it uses HTMX.

Example

class MyFragment(FragmentMixin, FormView):
    template_name = "app/fragment.jinja"
    form_class = MyForm
    success_url = reverse_lazy("foo:bar")

# in another view :
def some_view(request):
    fragment = MyFragment.as_fragment()
    return render(
        request,
        "app/template.jinja",
        context={"fragment": fragment(request)
    }

# in urls.py
urlpatterns = [
    path("foo/view", some_view),
    path("foo/fragment", MyFragment.as_view()),
]

reload_on_redirect = False class-attribute instance-attribute

If True, this fragment will trigger a full page reload on redirect.

UseFragmentsMixin

Bases: ContextMixin

Mark a view as using fragments.

This mixin is not mandatory (you may as well render manually your fragments in the get_context_data method). However, the interface of this class bring some distinction between fragments and other context data, which may reduce boilerplate.

Example

class FooFragment(FragmentMixin, FormView): ...

class BarFragment(FragmentMixin, FormView): ...

class AdminFragment(FragmentMixin, FormView): ...

class MyView(UseFragmentsMixin, TemplateView)
    template_name = "app/view.jinja"
    fragments = {
        "foo": FooFragment
        "bar": BarFragment(template_name="some_template.jinja")
    }
    fragments_data = {
        "foo": {"some": "data"}  # this will be passed to the FooFragment renderer
    }

    def get_fragments(self):
        res = super().get_fragments()
        if self.request.user.is_superuser:
            res["admin_fragment"] = AdminFragment
        return res

get_fragment_data()

Return eventual data used to initialize the fragments.

Source code in core/views/mixins.py
def get_fragment_data(self) -> dict[str, dict[str, Any]]:
    """Return eventual data used to initialize the fragments."""
    return self.fragment_data if self.fragment_data is not None else {}

get_fragment_context_data()

Return the rendered fragments as context data.

Source code in core/views/mixins.py
def get_fragment_context_data(self) -> dict[str, SafeString]:
    """Return the rendered fragments as context data."""
    res = {}
    data = self.get_fragment_data()
    for name, fragment in self.get_fragments().items():
        is_cls = inspect.isclass(fragment) and issubclass(fragment, FragmentMixin)
        _fragment = fragment.as_fragment() if is_cls else fragment
        fragment_data = data.get(name, {})
        res[name] = _fragment(self.request, **fragment_data)
    return res

BaseBasketForm

Bases: BaseFormSet

BasketItemForm(customer, counter, allowed_prices, *args, **kwargs)

Bases: Form

Source code in counter/forms.py
def __init__(
    self,
    customer: Customer,
    counter: Counter,
    allowed_prices: dict[int, Price],
    *args,
    **kwargs,
):
    self.customer = customer  # Used by formset
    self.counter = counter  # Used by formset
    self.allowed_prices = allowed_prices
    super().__init__(*args, **kwargs)

BillingInfoForm

Bases: ModelForm

BillingInfo

Bases: Model

Represent the billing information of a user, which are required by the 3D-Secure v2 system used by the etransaction module.

to_3dsv2_xml()

Convert the data from this model into a xml usable by the online paying service of the Crédit Agricole bank. see : https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster.

Source code in counter/models.py
def to_3dsv2_xml(self) -> str:
    """Convert the data from this model into a xml usable
    by the online paying service of the Crédit Agricole bank.
    see : `https://www.ca-moncommerce.com/espace-client-mon-commerce/up2pay-e-transactions/ma-documentation/manuel-dintegration-focus-3ds-v2/principes-generaux/#integration-3dsv2-developpeur-webmaster`.
    """
    data = {
        "Address": {
            "FirstName": self.first_name,
            "LastName": self.last_name,
            "Address1": self.address_1,
            "ZipCode": self.zip_code,
            "City": self.city,
            "CountryCode": self.country.numeric,  # ISO-3166-1 numeric code
            "MobilePhone": self.phone_number.as_national.replace(" ", ""),
            "CountryCodeMobilePhone": f"+{self.phone_number.country_code}",
        }
    }
    if self.address_2:
        data["Address"]["Address2"] = self.address_2
    xml = dict2xml(data, wrap="Billing", newlines=False)
    return '<?xml version="1.0" encoding="UTF-8" ?>' + xml

Customer

Bases: Model

Customer data of a User.

It adds some basic customers' information, such as the account ID, and is used by other accounting classes as reference to the customer, rather than using User.

can_buy property

Check if whether this customer has the right to purchase any item.

save(*args, allow_negative=False, **kwargs)

is_selling : tell if the current action is a selling allow_negative : ignored if not a selling. Allow a selling to put the account in negative Those two parameters avoid blocking the save method of a customer if his account is negative.

Source code in counter/models.py
def save(self, *args, allow_negative=False, **kwargs):
    """is_selling : tell if the current action is a selling
    allow_negative : ignored if not a selling. Allow a selling to put the account in negative
    Those two parameters avoid blocking the save method of a customer if his account is negative.
    """
    if self.amount < 0 and not allow_negative:
        raise ValidationError(_("Not enough money"))
    super().save(*args, **kwargs)

update_returnable_balance()

Update all returnable balances of this user to their real amount.

Source code in counter/models.py
def update_returnable_balance(self):
    """Update all returnable balances of this user to their real amount."""

    def purchases_qs(outer_ref: Literal["product_id", "returned_product_id"]):
        return (
            Selling.objects.filter(customer=self, product=OuterRef(outer_ref))
            .values("product")
            .annotate(quantity=Sum("quantity", default=0))
            .values("quantity")
        )

    balances = (
        ReturnableProduct.objects.annotate_balance_for(self)
        .annotate(
            nb_cons=Coalesce(Subquery(purchases_qs("product_id")), 0),
            nb_dcons=Coalesce(Subquery(purchases_qs("returned_product_id")), 0),
        )
        .annotate(new_balance=F("nb_cons") - F("nb_dcons"))
        .values("id", "new_balance")
    )
    updated_balances = [
        ReturnableProductBalance(
            customer=self, returnable_id=b["id"], balance=b["new_balance"]
        )
        for b in balances
    ]
    ReturnableProductBalance.objects.bulk_create(
        updated_balances,
        update_conflicts=True,
        update_fields=["balance"],
        unique_fields=["customer", "returnable"],
    )

get_or_create(user) classmethod

Work in pretty much the same way as the usual get_or_create method, but with the default field replaced by some under the hood.

If the user has an account, return it as is. Else create a new account with no money on it and a new unique account id

Example : ::

user = User.objects.get(pk=1)
account, created = Customer.get_or_create(user)
if created:
    print(f"created a new account with id {account.id}")
else:
    print(f"user has already an account, with {account.id} € on it"
Source code in counter/models.py
@classmethod
def get_or_create(cls, user: User) -> tuple[Customer, bool]:
    """Work in pretty much the same way as the usual get_or_create method,
    but with the default field replaced by some under the hood.

    If the user has an account, return it as is.
    Else create a new account with no money on it and a new unique account id

    Example : ::

        user = User.objects.get(pk=1)
        account, created = Customer.get_or_create(user)
        if created:
            print(f"created a new account with id {account.id}")
        else:
            print(f"user has already an account, with {account.id} € on it"
    """
    if hasattr(user, "customer"):
        return user.customer, False

    # account_id are always a number with a letter appended
    account_id = (
        Customer.objects.order_by(Length("account_id"), "account_id")
        .values("account_id")
        .last()
    )
    if account_id is None:
        # legacy from the old site
        account = cls.objects.create(user=user, account_id="1504a")
        return account, True

    account_id = account_id["account_id"]
    account_num = int(account_id[:-1])
    while Customer.objects.filter(account_id=account_id).exists():
        # when entering the first iteration, we are using an already existing account id
        # so the loop should always execute at least one time
        account_num += 1
        account_id = f"{account_num}{random.choice(string.ascii_lowercase)}"

    account = cls.objects.create(user=user, account_id=account_id)
    return account, True

Price

Bases: Model

Refilling

Bases: Model

Handle the refilling.

Selling

Bases: Model

Handle the sellings.

save(*args, allow_negative=False, **kwargs)

allow_negative : Allow this selling to use more money than available for this user.

Source code in counter/models.py
def save(self, *args, allow_negative=False, **kwargs):
    """allow_negative : Allow this selling to use more money than available for this user."""
    if not self.date:
        self.date = timezone.now()
    self.full_clean()
    if (
        self._state.adding
        and self.payment_method == self.PaymentMethod.SITH_ACCOUNT
    ):
        self.customer.amount -= self.quantity * self.unit_price
        self.customer.save(allow_negative=allow_negative)
    user = self.customer.user
    if user.was_subscribed:
        if (
            self.product
            and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER
        ):
            sub = Subscription(
                member=user,
                subscription_type="un-semestre",
                payment_method="EBOUTIC",
                location="EBOUTIC",
            )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ]
            )
            sub.subscription_end = Subscription.compute_end(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ],
                start=sub.subscription_start,
            )
            sub.save()
        elif (
            self.product
            and self.product.id == settings.SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS
        ):
            sub = Subscription(
                member=user,
                subscription_type="deux-semestres",
                payment_method="EBOUTIC",
                location="EBOUTIC",
            )
            sub.subscription_start = Subscription.compute_start()
            sub.subscription_start = Subscription.compute_start(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ]
            )
            sub.subscription_end = Subscription.compute_end(
                duration=settings.SITH_SUBSCRIPTIONS[sub.subscription_type][
                    "duration"
                ],
                start=sub.subscription_start,
            )
            sub.save()
    if user.preferences.notify_on_click:
        Notification(
            user=user,
            url=reverse(
                "core:user_account_detail",
                kwargs={
                    "user_id": user.id,
                    "year": self.date.year,
                    "month": self.date.month,
                },
            ),
            param="%d x %s" % (self.quantity, self.label),
            type="SELLING",
        ).save()
    super().save(*args, **kwargs)
    if hasattr(self.product, "eticket"):
        self.send_mail_customer()

Basket

Bases: Model

Basket is built when the user connects to an eboutic page.

generate_sales(counter, seller, payment_method)

Generate a list of sold items corresponding to the items of this basket WITHOUT saving them NOR deleting the basket.

Example
counter = Counter.objects.get(name="Eboutic")
user = User.objects.get(username="bibou")
sales = basket.generate_sales(counter, user, Selling.PaymentMethod.SITH_ACCOUNT)
# here the basket is in the same state as before the method call

with transaction.atomic():
    for sale in sales:
        sale.save()
    basket.delete()
    # all the basket items are deleted by the on_delete=CASCADE relation
    # thus only the sales remain
Source code in eboutic/models.py
def generate_sales(
    self, counter, seller: User, payment_method: Selling.PaymentMethod
):
    """Generate a list of sold items corresponding to the items
    of this basket WITHOUT saving them NOR deleting the basket.

    Example:
        ```python
        counter = Counter.objects.get(name="Eboutic")
        user = User.objects.get(username="bibou")
        sales = basket.generate_sales(counter, user, Selling.PaymentMethod.SITH_ACCOUNT)
        # here the basket is in the same state as before the method call

        with transaction.atomic():
            for sale in sales:
                sale.save()
            basket.delete()
            # all the basket items are deleted by the on_delete=CASCADE relation
            # thus only the sales remain
        ```
    """
    customer = Customer.get_or_create(self.user)[0]
    return [
        Selling(
            label=item.label,
            counter=counter,
            club_id=item.product.club_id,
            product=item.product,
            seller=seller,
            customer=customer,
            unit_price=item.unit_price,
            quantity=item.quantity,
            payment_method=payment_method,
        )
        for item in self.items.select_related("product")
    ]

BasketItem

Bases: AbstractBaseItem

from_price(price, quantity, basket) classmethod

Create a BasketItem with the same characteristics as the product price passed in parameters, with the specified quantity.

Source code in eboutic/models.py
@classmethod
def from_price(cls, price: Price, quantity: int, basket: Basket):
    """Create a BasketItem with the same characteristics as the
    product price passed in parameters, with the specified quantity.
    """
    return cls(
        basket=basket,
        label=price.full_label,
        product_id=price.product_id,
        quantity=quantity,
        unit_price=price.amount,
    )

BillingInfoState

Bases: Enum

Invoice

Bases: Model

Invoices are generated once the payment has been validated.

InvoiceItem

BaseEbouticBasketForm

EbouticMainView

Bases: LoginRequiredMixin, FormView

Main view of the eboutic application.

The purchasable products are those of the eboutic which belong to a category of products of a product category (orphan products are inaccessible).

BillingInfoFormFragment

Bases: LoginRequiredMixin, FragmentMixin, SuccessMessageMixin, UpdateView

Update billing info

EbouticCheckout

Bases: CanViewMixin, UseFragmentsMixin, DetailView

EbouticPayWithSith

Bases: CanViewMixin, SingleObjectMixin, View

EtransactionAutoAnswer

Bases: View

EurockPartnerFragment

Bases: IsSubscriberMixin, TemplateView

get_eboutic()

Source code in counter/models.py
def get_eboutic() -> Counter:
    return Counter.objects.filter(type="EBOUTIC").order_by("id").first()

payment_result(request, result)

Source code in eboutic/views.py
@require_GET
@login_required
def payment_result(request, result: str) -> HttpResponse:
    context = {"success": result == "success"}
    return render(request, "eboutic/eboutic_payment_result.jinja", context)