mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 20:09:25 +00:00
Don't use cookies for processing eboutic baskets
This commit is contained in:
128
eboutic/forms.py
128
eboutic/forms.py
@ -1,128 +0,0 @@
|
||||
#
|
||||
# Copyright 2022
|
||||
# - Maréchal <thgirod@hotmail.com
|
||||
#
|
||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||
# http://ae.utbm.fr.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License a published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
from functools import cached_property
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from pydantic import ValidationError
|
||||
|
||||
from eboutic.models import get_eboutic_products
|
||||
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
|
||||
|
||||
|
||||
class BasketForm:
|
||||
"""Class intended to perform checks on the request sended to the server when
|
||||
the user submits his basket from /eboutic/.
|
||||
|
||||
Because it must check an unknown number of fields, coming from a cookie
|
||||
and needing some databases checks to be performed, inheriting from forms.Form
|
||||
or using formset would have been likely to end in a big ball of wibbly-wobbly hacky stuff.
|
||||
Thus this class is a pure standalone and performs its operations by its own means.
|
||||
However, it still tries to share some similarities with a standard django Form.
|
||||
|
||||
Examples:
|
||||
::
|
||||
|
||||
def my_view(request):
|
||||
form = BasketForm(request)
|
||||
form.clean()
|
||||
if form.is_valid():
|
||||
# perform operations
|
||||
else:
|
||||
errors = form.get_error_messages()
|
||||
|
||||
# return the cookie that was in the request, but with all
|
||||
# incorrects elements removed
|
||||
cookie = form.get_cleaned_cookie()
|
||||
|
||||
You can also use a little shortcut by directly calling `form.is_valid()`
|
||||
without calling `form.clean()`. In this case, the latter method shall be
|
||||
implicitly called.
|
||||
"""
|
||||
|
||||
def __init__(self, request: HttpRequest):
|
||||
self.user = request.user
|
||||
self.cookies = request.COOKIES
|
||||
self.error_messages = set()
|
||||
self.correct_items = []
|
||||
|
||||
def clean(self) -> None:
|
||||
"""Perform all the checks, but return nothing.
|
||||
To know if the form is valid, the `is_valid()` method must be used.
|
||||
|
||||
The form shall be considered as valid if it meets all the following conditions :
|
||||
- it contains a "basket_items" key in the cookies of the request given in the constructor
|
||||
- this cookie is a list of objects formatted this way : `[{'id': <int>, 'quantity': <int>,
|
||||
'name': <str>, 'unit_price': <float>}, ...]`. The order of the fields in each object does not matter
|
||||
- all the ids are positive integers
|
||||
- all the ids refer to products available in the EBOUTIC
|
||||
- all the ids refer to products the user is allowed to buy
|
||||
- all the quantities are positive integers
|
||||
"""
|
||||
try:
|
||||
basket = PurchaseItemList.validate_json(
|
||||
unquote(self.cookies.get("basket_items", "[]"))
|
||||
)
|
||||
except ValidationError:
|
||||
self.error_messages.add(_("The request was badly formatted."))
|
||||
return
|
||||
if len(basket) == 0:
|
||||
self.error_messages.add(_("Your basket is empty."))
|
||||
return
|
||||
existing_ids = {product.id for product in get_eboutic_products(self.user)}
|
||||
for item in basket:
|
||||
# check a product with this id does exist
|
||||
if item.product_id in existing_ids:
|
||||
self.correct_items.append(item)
|
||||
else:
|
||||
self.error_messages.add(
|
||||
_(
|
||||
"%(name)s : this product does not exist or may no longer be available."
|
||||
)
|
||||
% {"name": item.name}
|
||||
)
|
||||
continue
|
||||
# this function does not return anything.
|
||||
# instead, it fills a set containing the collected error messages
|
||||
# an empty set means that no error was seen thus everything is ok
|
||||
# and the form is valid.
|
||||
# a non-empty set means there was at least one error thus
|
||||
# the form is invalid
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
"""Return True if the form is correct else False.
|
||||
|
||||
If the `clean()` method has not been called beforehand, call it.
|
||||
"""
|
||||
if not self.error_messages and not self.correct_items:
|
||||
self.clean()
|
||||
return not self.error_messages
|
||||
|
||||
@cached_property
|
||||
def errors(self) -> list[str]:
|
||||
return list(self.error_messages)
|
||||
|
||||
@cached_property
|
||||
def cleaned_data(self) -> list[PurchaseItemSchema]:
|
||||
return self.correct_items
|
@ -40,6 +40,7 @@ def get_eboutic_products(user: User) -> list[Product]:
|
||||
.annotate(order=F("product_type__order"))
|
||||
.annotate(category=F("product_type__name"))
|
||||
.annotate(category_comment=F("product_type__comment"))
|
||||
.annotate(price=F("selling_price"))
|
||||
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
|
||||
)
|
||||
return [p for p in products if p.can_be_sold_to(user)]
|
||||
@ -84,6 +85,9 @@ class Basket(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
return self.user == user
|
||||
|
||||
@cached_property
|
||||
def contains_refilling_item(self) -> bool:
|
||||
return self.items.filter(
|
||||
@ -139,7 +143,7 @@ class Basket(models.Model):
|
||||
club=product.club,
|
||||
product=product,
|
||||
seller=seller,
|
||||
customer=self.user.customer,
|
||||
customer=Customer.get_or_create(self.user)[0],
|
||||
unit_price=item.product_unit_price,
|
||||
quantity=item.quantity,
|
||||
payment_method=payment_method,
|
||||
|
@ -8,55 +8,44 @@ interface BasketItem {
|
||||
unit_price: number;
|
||||
}
|
||||
|
||||
const BASKET_ITEMS_COOKIE_NAME: string = "basket_items";
|
||||
|
||||
/**
|
||||
* Search for a cookie by name
|
||||
* @param name Name of the cookie to get
|
||||
* @returns the value of the cookie or null if it does not exist, undefined if not found
|
||||
*/
|
||||
function getCookie(name: string): string | null | undefined {
|
||||
if (!document.cookie || document.cookie.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const found = document.cookie
|
||||
.split(";")
|
||||
.map((c) => c.trim())
|
||||
.find((c) => c.startsWith(`${name}=`));
|
||||
|
||||
return found === undefined ? undefined : decodeURIComponent(found.split("=")[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the basket items from the associated cookie
|
||||
* @returns the items in the basket
|
||||
*/
|
||||
function getStartingItems(): BasketItem[] {
|
||||
const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME);
|
||||
if (!cookie) {
|
||||
return [];
|
||||
}
|
||||
// Django cookie backend converts `,` to `\054`
|
||||
let parsed = JSON.parse(cookie.replace(/\\054/g, ","));
|
||||
if (typeof parsed === "string") {
|
||||
// In some conditions, a second parsing is needed
|
||||
parsed = JSON.parse(parsed);
|
||||
}
|
||||
const res = Array.isArray(parsed) ? parsed : [];
|
||||
return res.filter((i) => !!document.getElementById(i.id));
|
||||
}
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("basket", () => ({
|
||||
items: getStartingItems() as BasketItem[],
|
||||
basket: [] as BasketItem[],
|
||||
|
||||
init() {
|
||||
this.basket = this.loadBasket();
|
||||
this.$watch("basket", () => {
|
||||
this.saveBasket();
|
||||
});
|
||||
|
||||
// It's quite tricky to manually apply attributes to the management part
|
||||
// of a formset so we dynamically apply it here
|
||||
this.$refs.basketManagementForm
|
||||
.querySelector("#id_form-TOTAL_FORMS")
|
||||
.setAttribute(":value", "basket.length");
|
||||
},
|
||||
|
||||
loadBasket(): BasketItem[] {
|
||||
if (localStorage.basket === undefined) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return JSON.parse(localStorage.basket);
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
saveBasket() {
|
||||
localStorage.basket = JSON.stringify(this.basket);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the total price of the basket
|
||||
* @returns {number} The total price of the basket
|
||||
*/
|
||||
getTotal() {
|
||||
return this.items.reduce(
|
||||
return this.basket.reduce(
|
||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unit_price,
|
||||
0,
|
||||
);
|
||||
@ -68,7 +57,6 @@ document.addEventListener("alpine:init", () => {
|
||||
*/
|
||||
add(item: BasketItem) {
|
||||
item.quantity++;
|
||||
this.setCookies();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -76,39 +64,25 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param itemId the id of the item to remove
|
||||
*/
|
||||
remove(itemId: number) {
|
||||
const index = this.items.findIndex((e: BasketItem) => e.id === itemId);
|
||||
const index = this.basket.findIndex((e: BasketItem) => e.id === itemId);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
this.items[index].quantity -= 1;
|
||||
this.basket[index].quantity -= 1;
|
||||
|
||||
if (this.items[index].quantity === 0) {
|
||||
this.items = this.items.filter(
|
||||
(e: BasketItem) => e.id !== this.items[index].id,
|
||||
if (this.basket[index].quantity === 0) {
|
||||
this.basket = this.basket.filter(
|
||||
(e: BasketItem) => e.id !== this.basket[index].id,
|
||||
);
|
||||
}
|
||||
this.setCookies();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all the items from the basket & cleans the catalog CSS classes
|
||||
* Remove all the basket from the basket & cleans the catalog CSS classes
|
||||
*/
|
||||
clearBasket() {
|
||||
this.items = [];
|
||||
this.setCookies();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the cookie in the browser with the basket items
|
||||
* ! the cookie survives an hour
|
||||
*/
|
||||
setCookies() {
|
||||
if (this.items.length === 0) {
|
||||
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`;
|
||||
} else {
|
||||
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.items))};Max-Age=3600`;
|
||||
}
|
||||
this.basket = [];
|
||||
},
|
||||
|
||||
/**
|
||||
@ -127,7 +101,7 @@ document.addEventListener("alpine:init", () => {
|
||||
unit_price: price,
|
||||
} as BasketItem;
|
||||
|
||||
this.items.push(newItem);
|
||||
this.basket.push(newItem);
|
||||
this.add(newItem);
|
||||
|
||||
return newItem;
|
||||
@ -141,7 +115,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param price The unit price of the product
|
||||
*/
|
||||
addFromCatalog(id: number, name: string, price: number) {
|
||||
let item = this.items.find((e: BasketItem) => e.id === id);
|
||||
let item = this.basket.find((e: BasketItem) => e.id === id);
|
||||
|
||||
// if the item is not in the basket, we create it
|
||||
// else we add + 1 to it
|
||||
|
@ -24,48 +24,72 @@
|
||||
<div id="eboutic" x-data="basket">
|
||||
<div id="basket">
|
||||
<h3>Panier</h3>
|
||||
{% if errors %}
|
||||
<div class="alert alert-red">
|
||||
<div class="alert-main">
|
||||
{% for error in errors %}
|
||||
<p style="margin: 0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div x-ref="basketManagementForm">
|
||||
{{ form.management_form }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="item-list">
|
||||
{# Starting money #}
|
||||
<li>
|
||||
<span class="item-name">
|
||||
<strong>{% trans %}Current account amount: {% endtrans %}</strong>
|
||||
</span>
|
||||
<span class="item-price">
|
||||
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||
</span>
|
||||
</li>
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li class="item-row" x-show="item.quantity > 0">
|
||||
<div class="item-quantity">
|
||||
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||
<span x-text="item.quantity"></span>
|
||||
<i class="fa fa-plus" @click="add(item)"></i>
|
||||
|
||||
{% if form.non_form_errors() %}
|
||||
<div class="alert alert-red">
|
||||
<div class="alert-main">
|
||||
{% for error in form.non_form_errors() %}
|
||||
<p style="margin: 0">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="item-name" x-text="item.name"></span>
|
||||
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<ul class="item-list">
|
||||
{# Starting money #}
|
||||
<li>
|
||||
<span class="item-name">
|
||||
<strong>{% trans %}Current account amount: {% endtrans %}</strong>
|
||||
</span>
|
||||
<span class="item-price">
|
||||
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
{# Total price #}
|
||||
<li style="margin-top: 20px">
|
||||
<span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span>
|
||||
<span x-text="getTotal().toFixed(2) + ' €'" class="item-price"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="catalog-buttons">
|
||||
<button @click="clearBasket()" class="btn btn-grey">
|
||||
<i class="fa fa-trash"></i>
|
||||
{% trans %}Clear{% endtrans %}
|
||||
</button>
|
||||
<form method="get" action="{{ url('eboutic:command') }}">
|
||||
|
||||
<template x-for="(item, index) in Object.values(basket)">
|
||||
<li class="item-row" x-show="item.quantity > 0">
|
||||
<div class="item-quantity">
|
||||
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||
<span x-text="item.quantity"></span>
|
||||
<i class="fa fa-plus" @click="add(item)"></i>
|
||||
</div>
|
||||
<span class="item-name" x-text="item.name"></span>
|
||||
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.quantity"
|
||||
:id="`id_form-${index}-quantity`"
|
||||
:name="`form-${index}-quantity`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.id"
|
||||
:id="`id_form-${index}-id`"
|
||||
:name="`form-${index}-id`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
|
||||
</li>
|
||||
</template>
|
||||
{# Total price #}
|
||||
<li style="margin-top: 20px">
|
||||
<span class="item-name"><strong>{% trans %}Basket amount: {% endtrans %}</strong></span>
|
||||
<span x-text="getTotal().toFixed(2) + ' €'" class="item-price"></span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="catalog-buttons">
|
||||
<button @click.prevent="clearBasket()" class="btn btn-grey">
|
||||
<i class="fa fa-trash"></i>
|
||||
{% trans %}Clear{% endtrans %}
|
||||
</button>
|
||||
<button class="btn btn-blue">
|
||||
<i class="fa fa-check"></i>
|
||||
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
|
||||
@ -158,7 +182,7 @@
|
||||
<button
|
||||
id="{{ p.id }}"
|
||||
class="card product-button clickable shadow"
|
||||
:class="{selected: items.some((i) => i.id === {{ p.id }})}"
|
||||
:class="{selected: basket.some((i) => i.id === {{ p.id }})}"
|
||||
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
|
||||
>
|
||||
{% if p.icon %}
|
||||
|
@ -82,9 +82,8 @@
|
||||
{% elif basket.total > user.account_balance %}
|
||||
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
|
||||
{% else %}
|
||||
<form method="post" action="{{ url('eboutic:pay_with_sith') }}" name="sith-pay-form">
|
||||
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="pay_with_sith_account">
|
||||
<input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -28,10 +28,10 @@ from eboutic.converters import PaymentResultConverter
|
||||
from eboutic.views import (
|
||||
BillingInfoFormFragment,
|
||||
EbouticCommand,
|
||||
EbouticCreateBasket,
|
||||
EbouticPayWithSith,
|
||||
EtransactionAutoAnswer,
|
||||
EurokPartnerFragment,
|
||||
eboutic_main,
|
||||
pay_with_sith,
|
||||
payment_result,
|
||||
)
|
||||
|
||||
@ -39,10 +39,12 @@ register_converter(PaymentResultConverter, "res")
|
||||
|
||||
urlpatterns = [
|
||||
# Subscription views
|
||||
path("", eboutic_main, name="main"),
|
||||
path("command/", EbouticCommand.as_view(), name="command"),
|
||||
path("", EbouticCreateBasket.as_view(), name="main"),
|
||||
path("command/<int:basket_id>", EbouticCommand.as_view(), name="command"),
|
||||
path("billing-infos/", BillingInfoFormFragment.as_view(), name="billing_infos"),
|
||||
path("pay/sith/", pay_with_sith, name="pay_with_sith"),
|
||||
path(
|
||||
"pay/sith/<int:basket_id>", EbouticPayWithSith.as_view(), name="pay_with_sith"
|
||||
),
|
||||
path("pay/<res:result>/", payment_result, name="payment_result"),
|
||||
path("eurok/", EurokPartnerFragment.as_view(), name="eurok"),
|
||||
path(
|
||||
|
194
eboutic/views.py
194
eboutic/views.py
@ -35,21 +35,21 @@ from django.contrib.auth.mixins import (
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.db.models.fields import forms
|
||||
from django.db.utils import cached_property
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.http import require_GET, require_POST
|
||||
from django.views.generic import TemplateView, UpdateView, View
|
||||
from django.views.decorators.http import require_GET
|
||||
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
|
||||
from django.views.generic.edit import SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
from core.auth.mixins import IsSubscriberMixin
|
||||
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
|
||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||
from counter.forms import BillingInfoForm
|
||||
from counter.forms import BaseBasketForm, BillingInfoForm, ProductForm
|
||||
from counter.models import BillingInfo, Counter, Customer, Product
|
||||
from eboutic.forms import BasketForm
|
||||
from eboutic.models import (
|
||||
Basket,
|
||||
BasketItem,
|
||||
@ -58,39 +58,69 @@ from eboutic.models import (
|
||||
InvoiceItem,
|
||||
get_eboutic_products,
|
||||
)
|
||||
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
from django.utils.html import SafeString
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def eboutic_main(request: HttpRequest) -> HttpResponse:
|
||||
"""Main view of the eboutic application.
|
||||
class BaseEbouticBasketForm(BaseBasketForm):
|
||||
def _check_enough_money(self, *args, **kwargs):
|
||||
# Disable money check
|
||||
...
|
||||
|
||||
Return an Http response whose content is of type text/html.
|
||||
The latter represents the page from which a user can see
|
||||
the catalogue of products that he can buy and fill
|
||||
his shopping cart.
|
||||
|
||||
EbouticBasketForm = forms.formset_factory(
|
||||
ProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||
)
|
||||
|
||||
|
||||
class EbouticCreateBasket(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).
|
||||
|
||||
If the session contains a key-value pair that associates "errors"
|
||||
with a list of strings, this pair is removed from the session
|
||||
and its value displayed to the user when the page is rendered.
|
||||
"""
|
||||
errors = request.session.pop("errors", None)
|
||||
products = get_eboutic_products(request.user)
|
||||
context = {
|
||||
"errors": errors,
|
||||
"products": products,
|
||||
"customer_amount": request.user.account_balance,
|
||||
}
|
||||
return render(request, "eboutic/eboutic_main.jinja", context)
|
||||
|
||||
template_name = "eboutic/eboutic_main.jinja"
|
||||
form_class = EbouticBasketForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["form_kwargs"] = {
|
||||
"customer": Customer.get_or_create(self.request.user)[0],
|
||||
"counter": Counter.objects.get(type="EBOUTIC"),
|
||||
"allowed_products": {product.id: product for product in self.products},
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, formset):
|
||||
if len(formset) == 0:
|
||||
return self.form_invalid(formset)
|
||||
|
||||
with transaction.atomic():
|
||||
self.basket = Basket.objects.create(user=self.request.user)
|
||||
for form in formset:
|
||||
BasketItem.from_product(
|
||||
form.product, form.cleaned_data["quantity"], self.basket
|
||||
).save()
|
||||
self.basket.save()
|
||||
return super().form_valid(formset)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("eboutic:command", kwargs={"basket_id": self.basket.id})
|
||||
|
||||
@cached_property
|
||||
def products(self) -> list[Product]:
|
||||
return get_eboutic_products(self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["products"] = self.products
|
||||
context["customer_amount"] = self.request.user.account_balance
|
||||
return context
|
||||
|
||||
|
||||
@require_GET
|
||||
@ -166,48 +196,15 @@ class BillingInfoFormFragment(
|
||||
return self.request.path
|
||||
|
||||
|
||||
class EbouticCommand(LoginRequiredMixin, UseFragmentsMixin, TemplateView):
|
||||
class EbouticCommand(CanViewMixin, UseFragmentsMixin, DetailView):
|
||||
model = Basket
|
||||
pk_url_kwarg = "basket_id"
|
||||
context_object_name = "basket"
|
||||
template_name = "eboutic/eboutic_makecommand.jinja"
|
||||
basket: Basket
|
||||
fragments = {
|
||||
"billing_infos_form": BillingInfoFormFragment,
|
||||
}
|
||||
|
||||
@method_decorator(login_required)
|
||||
def post(self, request, *args, **kwargs):
|
||||
return redirect("eboutic:main")
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs):
|
||||
form = BasketForm(request)
|
||||
if not form.is_valid():
|
||||
request.session["errors"] = form.errors
|
||||
request.session.modified = True
|
||||
res = redirect("eboutic:main")
|
||||
res.set_cookie(
|
||||
"basket_items",
|
||||
PurchaseItemList.dump_json(form.cleaned_data, by_alias=True).decode(),
|
||||
path="/eboutic",
|
||||
)
|
||||
return res
|
||||
basket = Basket.from_session(request.session)
|
||||
if basket is not None:
|
||||
basket.items.all().delete()
|
||||
else:
|
||||
basket = Basket.objects.create(user=request.user)
|
||||
request.session["basket_id"] = basket.id
|
||||
request.session.modified = True
|
||||
|
||||
items: list[PurchaseItemSchema] = form.cleaned_data
|
||||
pks = {item.product_id for item in items}
|
||||
products = {p.pk: p for p in Product.objects.filter(pk__in=pks)}
|
||||
db_items = []
|
||||
for pk in pks:
|
||||
quantity = sum(i.quantity for i in items if i.product_id == pk)
|
||||
db_items.append(BasketItem.from_product(products[pk], quantity, basket))
|
||||
BasketItem.objects.bulk_create(db_items)
|
||||
self.basket = basket
|
||||
return super().get(request)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if hasattr(self.request.user, "customer"):
|
||||
@ -215,51 +212,48 @@ class EbouticCommand(LoginRequiredMixin, UseFragmentsMixin, TemplateView):
|
||||
kwargs["customer_amount"] = customer.amount
|
||||
else:
|
||||
kwargs["customer_amount"] = None
|
||||
kwargs["basket"] = self.basket
|
||||
kwargs["billing_infos"] = {}
|
||||
|
||||
with contextlib.suppress(BillingInfo.DoesNotExist):
|
||||
kwargs["billing_infos"] = json.dumps(
|
||||
dict(self.basket.get_e_transaction_data())
|
||||
dict(self.object.get_e_transaction_data())
|
||||
)
|
||||
return kwargs
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def pay_with_sith(request):
|
||||
basket = Basket.from_session(request.session)
|
||||
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
if basket is None or basket.items.filter(type_id=refilling).exists():
|
||||
return redirect("eboutic:main")
|
||||
c = Customer.objects.filter(user__id=basket.user_id).first()
|
||||
if c is None:
|
||||
return redirect("eboutic:main")
|
||||
if c.amount < basket.total:
|
||||
res = redirect("eboutic:payment_result", "failure")
|
||||
res.delete_cookie("basket_items", "/eboutic")
|
||||
return res
|
||||
eboutic = Counter.objects.get(type="EBOUTIC")
|
||||
sales = basket.generate_sales(eboutic, c.user, "SITH_ACCOUNT")
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Selling.save has some important business logic in it.
|
||||
# Do not bulk_create this
|
||||
for sale in sales:
|
||||
sale.save()
|
||||
basket.delete()
|
||||
request.session.pop("basket_id", None)
|
||||
res = redirect("eboutic:payment_result", "success")
|
||||
except DatabaseError as e:
|
||||
with sentry_sdk.push_scope() as scope:
|
||||
scope.user = {"username": request.user.username}
|
||||
scope.set_extra("someVariable", e.__repr__())
|
||||
sentry_sdk.capture_message(
|
||||
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
|
||||
class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
||||
http_method_names = ["post"]
|
||||
model = Basket
|
||||
pk_url_kwarg = "basket_id"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
basket = self.get_object()
|
||||
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
if basket.items.filter(type_id=refilling).exists():
|
||||
messages.error(
|
||||
self.request,
|
||||
_("You can't buy a refilling with sith money"),
|
||||
)
|
||||
res = redirect("eboutic:payment_result", "failure")
|
||||
res.delete_cookie("basket_items", "/eboutic")
|
||||
return res
|
||||
return redirect("eboutic:main")
|
||||
|
||||
eboutic = Counter.objects.get(type="EBOUTIC")
|
||||
sales = basket.generate_sales(eboutic, basket.user, "SITH_ACCOUNT")
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Selling.save has some important business logic in it.
|
||||
# Do not bulk_create this
|
||||
for sale in sales:
|
||||
sale.save()
|
||||
basket.delete()
|
||||
return redirect("eboutic:payment_result", "success")
|
||||
except DatabaseError as e:
|
||||
with sentry_sdk.push_scope() as scope:
|
||||
scope.user = {"username": request.user.username}
|
||||
scope.set_extra("someVariable", e.__repr__())
|
||||
sentry_sdk.capture_message(
|
||||
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
|
||||
)
|
||||
return redirect("eboutic:payment_result", "failure")
|
||||
|
||||
|
||||
class EtransactionAutoAnswer(View):
|
||||
|
Reference in New Issue
Block a user