mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-12 04:49:25 +00:00
refonte de la boutique en ligne
This commit is contained in:
175
eboutic/forms.py
Normal file
175
eboutic/forms.py
Normal file
@ -0,0 +1,175 @@
|
||||
# -*- coding:utf-8 -*
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
#
|
||||
|
||||
import json
|
||||
import re
|
||||
import typing
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from eboutic.models import get_eboutic_products
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# check the json is an array containing non-nested objects.
|
||||
# values must be strings or numbers
|
||||
# this is matched :
|
||||
# [{"id": 4, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||
# but this is not :
|
||||
# [{"id": {"nested_id": 10}, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||
# and neither does this :
|
||||
# [{"id": ["nested_id": 10], "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||
# and neither does that :
|
||||
# [{"id": null, "name": "[PROMO 22] badges", "unit_price": 2.3, "quantity": 2}]
|
||||
json_cookie_re = re.compile(
|
||||
r"^\[\s*(\{\s*(\"[^\"]*\":\s*(\"[^\"]{0,64}\"|\d{0,5}\.?\d+),?\s*)*\},?\s*)*\s*\]$"
|
||||
)
|
||||
|
||||
def __init__(self, request: HttpRequest):
|
||||
self.user = request.user
|
||||
self.cookies = request.COOKIES
|
||||
self.error_messages = set()
|
||||
self.correct_cookie = []
|
||||
|
||||
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
|
||||
"""
|
||||
basket = self.cookies.get("basket_items", None)
|
||||
if basket is None or basket in ("[]", ""):
|
||||
self.error_messages.add(_("You have no basket."))
|
||||
return
|
||||
# check that the json is not nested before parsing it to make sure
|
||||
# malicious user can't ddos the server with deeply nested json
|
||||
if not BasketForm.json_cookie_re.match(basket):
|
||||
self.error_messages.add(_("The request was badly formatted."))
|
||||
return
|
||||
try:
|
||||
basket = json.loads(basket)
|
||||
except json.JSONDecodeError:
|
||||
self.error_messages.add(_("The basket cookie was badly formatted."))
|
||||
return
|
||||
if type(basket) is not list or len(basket) == 0:
|
||||
self.error_messages.add(_("Your basket is empty."))
|
||||
return
|
||||
for item in basket:
|
||||
expected_keys = {"id", "quantity", "name", "unit_price"}
|
||||
if type(item) is not dict or set(item.keys()) != expected_keys:
|
||||
self.error_messages.add("One or more items are badly formatted.")
|
||||
continue
|
||||
# check the id field is a positive integer
|
||||
if type(item["id"]) is not int or item["id"] < 0:
|
||||
self.error_messages.add(
|
||||
_("%(name)s : this product does not exist.")
|
||||
% {"name": item["name"]}
|
||||
)
|
||||
continue
|
||||
# check a product with this id does exist
|
||||
ids = {product.id for product in get_eboutic_products(self.user)}
|
||||
if not item["id"] in ids:
|
||||
self.error_messages.add(
|
||||
_(
|
||||
"%(name)s : this product does not exist or may no longer be available."
|
||||
)
|
||||
% {"name": item["name"]}
|
||||
)
|
||||
continue
|
||||
if type(item["quantity"]) is not int or item["quantity"] < 0:
|
||||
self.error_messages.add(
|
||||
_("You cannot buy %(nbr)d %(name)%s.")
|
||||
% {"nbr": item["quantity"], "name": item["name"]}
|
||||
)
|
||||
continue
|
||||
|
||||
# if we arrive here, it means this item has passed all tests
|
||||
self.correct_cookie.append(item)
|
||||
# for loop for item checking ends here
|
||||
|
||||
# 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 self.error_messages == set() and self.correct_cookie == []:
|
||||
self.clean()
|
||||
if self.error_messages:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_error_messages(self) -> typing.List[str]:
|
||||
# return [msg for msg in self.error_messages]
|
||||
return list(self.error_messages)
|
||||
|
||||
def get_cleaned_cookie(self) -> str:
|
||||
if not self.correct_cookie:
|
||||
return ""
|
||||
return json.dumps(self.correct_cookie)
|
Reference in New Issue
Block a user