mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 00:53:08 +00:00 
			
		
		
		
	Implémentation 3DSv2 + résolution bugs eboutic + amélioration pages admin (#558)
Eboutic : - Implémentation de la norme 3DSecure v2 pour les paiement par carte bancaire - Amélioration générale de l'interface utilisateur - Résolution du problème avec les caractères spéciaux dans le panier sur Safari - Réparation du cookie du panier de l'eboutic qui n'était pas fonctionnel Autre : - Mise à jour de la documentation - Mise à jour des dépendances Javascript - Suppression du code inutilisé dans `subscription/models.py` - Amélioration des pages administrateur (back-office Django) Co-authored-by: thomas girod <56346771+imperosol@users.noreply.github.com> Co-authored-by: Théo DURR <git@theodurr.fr> Co-authored-by: Julien Constant <julienconstant190@gmail.com>
This commit is contained in:
		
							
								
								
									
										263
									
								
								eboutic/static/eboutic/css/eboutic.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								eboutic/static/eboutic/css/eboutic.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,263 @@ | ||||
| #eboutic { | ||||
|     display: flex; | ||||
|     flex-direction: row-reverse; | ||||
|     align-items: flex-start; | ||||
|     column-gap: 20px; | ||||
|     margin: 0 20px 20px; | ||||
| } | ||||
|  | ||||
| #eboutic-title { | ||||
|     margin-left: 20px; | ||||
| } | ||||
|  | ||||
| #eboutic h3 { | ||||
|     margin-left: 0; | ||||
|     margin-right: 0; | ||||
| } | ||||
|  | ||||
| #basket { | ||||
|     min-width: 300px; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
|     padding: 10px; | ||||
| } | ||||
|  | ||||
| #basket h3 { | ||||
|     margin-top: 0; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 765px) { | ||||
|     #eboutic { | ||||
|         flex-direction: column-reverse; | ||||
|         align-items: center; | ||||
|         margin: 10px; | ||||
|         row-gap: 20px; | ||||
|     } | ||||
|     #eboutic-title { | ||||
|         margin-bottom: 20px; | ||||
|         margin-top: 4px; | ||||
|     } | ||||
|     #basket { | ||||
|         width: -webkit-fill-available; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #eboutic .item-list { | ||||
|     margin-left: 0; | ||||
|     list-style: none; | ||||
| } | ||||
|  | ||||
| #eboutic .item-list li { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     margin-bottom: 10px | ||||
| } | ||||
|  | ||||
| #eboutic .item-row { | ||||
|     gap: 10px; | ||||
| } | ||||
|  | ||||
| #eboutic .item-name { | ||||
|     word-break: break-word; | ||||
|     width: 100%; | ||||
|     line-height: 100%; | ||||
| } | ||||
|  | ||||
| #eboutic .fa-plus, | ||||
| #eboutic .fa-minus { | ||||
|     cursor: pointer; | ||||
|     background-color: #354a5f; | ||||
|     color: white; | ||||
|     border-radius: 50%; | ||||
|     padding: 5px; | ||||
|     font-size: 10px; | ||||
|     line-height: 10px; | ||||
|     width: 10px; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| #eboutic .item-quantity { | ||||
|     min-width: 65px; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     display: flex; | ||||
|     gap: 5px; | ||||
| } | ||||
|  | ||||
| #eboutic .item-price { | ||||
|     min-width: 65px; | ||||
|     text-align: right; | ||||
| } | ||||
|  | ||||
| /*  CSS du catalogue  */ | ||||
|  | ||||
| #eboutic #catalog { | ||||
|     display: flex; | ||||
|     flex-grow: 1; | ||||
|     flex-direction: column; | ||||
|     row-gap: 30px; | ||||
| } | ||||
|  | ||||
| #eboutic .category-header { | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| #eboutic .product-group { | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|     column-gap: 15px; | ||||
|     row-gap: 15px; | ||||
| } | ||||
| #eboutic .product-button { | ||||
|     position: relative; | ||||
|     box-sizing: border-box; | ||||
|     min-height: 180px; | ||||
|     height: fit-content; | ||||
|     width: 150px; | ||||
|     padding: 15px; | ||||
|     overflow: hidden; | ||||
|     box-shadow: rgb(60 64 67 / 30%) 0 1px 3px 0, rgb(60 64 67 / 15%) 0 4px 8px 3px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     row-gap: 5px; | ||||
|     justify-content: flex-start; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button.selected { | ||||
|     animation: bg-in-out 1s ease; | ||||
|     background-color: rgb(216, 236, 255); | ||||
| } | ||||
|  | ||||
| #eboutic .product-button.selected::after { | ||||
|     content: "🛒"; | ||||
|     position: absolute; | ||||
|     top: 5px; | ||||
|     right: 5px; | ||||
|     padding: 5px; | ||||
|     border-radius: 50%; | ||||
|     box-shadow: 0px 0px 12px 2px rgb(0 0 0 / 14%); | ||||
|     background-color: white; | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
|     font-size: 16px; | ||||
|     line-height: 20px; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button:active { | ||||
|     box-shadow: none; | ||||
| } | ||||
|  | ||||
| #eboutic .product-image { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     min-height: 70px; | ||||
|     max-height: 70px; | ||||
|     object-fit: contain; | ||||
|     border-radius: 4px; | ||||
|     line-height: 70px; | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| #eboutic i.product-image { | ||||
|     background-color: rgba(173, 173, 173, 0.2); | ||||
| } | ||||
|  | ||||
| #eboutic .product-description h4 { | ||||
|     font-size: .75em; | ||||
|     word-break: break-word; | ||||
|     margin: 0 0 5px 0; | ||||
| } | ||||
|  | ||||
| #eboutic .product-button p { | ||||
|     font-size: 13px; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     column-gap: 30px; | ||||
|     margin: 30px 0 0; | ||||
| } | ||||
|  | ||||
| #eboutic input { | ||||
|     all: unset; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons button { | ||||
|     font-size: 15px!important; | ||||
|     font-weight: normal; | ||||
|     color: white; | ||||
|     min-width: 60px; | ||||
|     padding: 10px 15px; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons .validate { | ||||
|     background-color: #354a5f; | ||||
| } | ||||
| #eboutic .catalog-buttons .clear { | ||||
|     background-color: gray; | ||||
| } | ||||
| #eboutic .catalog-buttons button i { | ||||
|     margin-right: 4px; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons button.validate:hover { | ||||
|     background-color: #2c3646; | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons button.clear:hover { | ||||
|     background-color:hsl(210,5%,30%); | ||||
| } | ||||
|  | ||||
| #eboutic .catalog-buttons form { | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width: 765px) { | ||||
|     #eboutic #catalog { | ||||
|         row-gap: 15px; | ||||
|         width: 100%; | ||||
|     } | ||||
|  | ||||
|     #eboutic section { | ||||
|         text-align: center; | ||||
|     } | ||||
|  | ||||
|     #eboutic .product-group { | ||||
|         justify-content: space-around; | ||||
|         flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     #eboutic .product-group .product-button { | ||||
|         min-height: 100px; | ||||
|         width: 100%; | ||||
|         max-width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 10px; | ||||
|     } | ||||
|  | ||||
|     #eboutic .product-group .product-description { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: flex-start; | ||||
|         width: 100%; | ||||
|     } | ||||
|  | ||||
|     #eboutic .product-description h4 { | ||||
|         text-align: left; | ||||
|         max-width: 90%; | ||||
|     } | ||||
|  | ||||
|     #eboutic .product-image { | ||||
|         margin-bottom: 0px; | ||||
|         max-width: 70px; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @keyframes bg-in-out { | ||||
|     0%   { background-color: white; }  | ||||
|     100% { background-color: rgb(216, 236, 255); } | ||||
| } | ||||
							
								
								
									
										157
									
								
								eboutic/static/eboutic/js/eboutic.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								eboutic/static/eboutic/js/eboutic.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| /** | ||||
|  * @typedef {Object} BasketItem An item in the basket | ||||
|  * @property {number} id The id of the product | ||||
|  * @property {string} name The name of the product | ||||
|  * @property {number} quantity The quantity of the product | ||||
|  * @property {number} unit_price The unit price of the product | ||||
|  */ | ||||
|  | ||||
| const BASKET_ITEMS_COOKIE_NAME = "basket_items"; | ||||
|  | ||||
| /** | ||||
|  * Search for a cookie by name | ||||
|  * @param {string} name Name of the cookie to get | ||||
|  * @returns {string|null|undefined} the value of the cookie or null if it does not exist, undefined if not found | ||||
|  */ | ||||
| function getCookie(name) { | ||||
|     if (!document.cookie || document.cookie.length === 0) return null; | ||||
|  | ||||
|     let 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 {BasketItem[]|[]} the items in the basket | ||||
|  */ | ||||
| function get_starting_items() { | ||||
|     const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME); | ||||
|     let output = []; | ||||
|  | ||||
|     try { | ||||
|         // Django cookie backend does an utter mess on non-trivial data types | ||||
|         // so we must perform a conversion of our own | ||||
|         const biscuit = JSON.parse(cookie.replace(/\\054/g, ',')); | ||||
|         output = Array.isArray(biscuit) ? biscuit : []; | ||||
|  | ||||
|     } catch (e) {} | ||||
|  | ||||
|     output.forEach(item => { | ||||
|         let el = document.getElementById(item.id); | ||||
|         el.classList.add("selected"); | ||||
|     }); | ||||
|  | ||||
|     return output; | ||||
| } | ||||
|  | ||||
| document.addEventListener('alpine:init', () => { | ||||
|     Alpine.data('basket', () => ({ | ||||
|         items: get_starting_items(), | ||||
|  | ||||
|         /** | ||||
|          * Get the total price of the basket | ||||
|          * @returns {number} The total price of the basket | ||||
|          */ | ||||
|         get_total() { | ||||
|             return this.items | ||||
|                 .reduce((acc, item) => acc + item["quantity"] * item["unit_price"], 0); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Add 1 to the quantity of an item in the basket | ||||
|          * @param {BasketItem} item  | ||||
|          */ | ||||
|         add(item) { | ||||
|             item.quantity++; | ||||
|             this.set_cookies(); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Remove 1 to the quantity of an item in the basket | ||||
|          * @param {BasketItem} item_id  | ||||
|          */ | ||||
|         remove(item_id) { | ||||
|             const index = this.items.findIndex(e => e.id === item_id); | ||||
|              | ||||
|             if (index < 0) return; | ||||
|             this.items[index].quantity -= 1; | ||||
|  | ||||
|             if (this.items[index].quantity === 0) { | ||||
|                 let el = document.getElementById(this.items[index].id); | ||||
|                 el.classList.remove("selected"); | ||||
|  | ||||
|                 this.items = this.items.filter((e) => e.id !== this.items[index].id); | ||||
|             } | ||||
|             this.set_cookies(); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Remove all the items from the basket & cleans the catalog CSS classes | ||||
|          */ | ||||
|         clear_basket() { | ||||
|             // We remove the class "selected" from all the items in the catalog | ||||
|             this.items.forEach(item => { | ||||
|                 let el = document.getElementById(item.id); | ||||
|                 el.classList.remove("selected"); | ||||
|             }) | ||||
|  | ||||
|             this.items = []; | ||||
|             this.set_cookies(); | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Set the cookie in the browser with the basket items | ||||
|          * ! the cookie survives an hour | ||||
|          */ | ||||
|         set_cookies() { | ||||
|             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`; | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Create an item in the basket if it was not already in | ||||
|          * @param {number} id The id of the product to add | ||||
|          * @param {string} name The name of the product | ||||
|          * @param {number} price The unit price of the product | ||||
|          * @returns {BasketItem} The created item | ||||
|          */ | ||||
|         create_item(id, name, price) { | ||||
|             let new_item = { | ||||
|                 id: id, | ||||
|                 name: name, | ||||
|                 quantity: 0, | ||||
|                 unit_price: price | ||||
|             }; | ||||
|  | ||||
|             this.items.push(new_item); | ||||
|             this.add(new_item); | ||||
|  | ||||
|             return new_item; | ||||
|         }, | ||||
|  | ||||
|         /** | ||||
|          * Add an item to the basket. | ||||
|          * This is called when the user click on a button in the catalog | ||||
|          * @param {number} id The id of the product to add | ||||
|          * @param {string} name The name of the product | ||||
|          * @param {number} price The unit price of the product | ||||
|          */ | ||||
|         add_from_catalog(id, name, price) { | ||||
|             let item = this.items.find(e => e.id === id) | ||||
|  | ||||
|             // if the item is not in the basket, we create it | ||||
|             // else we add + 1 to it | ||||
|             if (item === undefined) item = this.create_item(id, name, price); | ||||
|             else this.add(item); | ||||
|  | ||||
|             if (item.quantity > 0) { | ||||
|                 let el = document.getElementById(item.id); | ||||
|                 el.classList.add("selected"); | ||||
|             } | ||||
|         }, | ||||
|     })) | ||||
| }) | ||||
							
								
								
									
										73
									
								
								eboutic/static/eboutic/js/makecommand.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								eboutic/static/eboutic/js/makecommand.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| document.addEventListener('alpine:init', () => { | ||||
|     Alpine.store('bank_payment_enabled', false) | ||||
|  | ||||
|     Alpine.store('billing_inputs', { | ||||
|         data: JSON.parse(et_data)["data"], | ||||
|  | ||||
|         async fill() { | ||||
|             document.getElementById("bank-submit-button").disabled = true; | ||||
|             const request = new Request(et_data_url, { | ||||
|                 method: "GET", | ||||
|                 headers: { | ||||
|                     'Accept': 'application/json', | ||||
|                     'Content-Type': 'application/json', | ||||
|                 }, | ||||
|             }); | ||||
|             const res = await fetch(request); | ||||
|             if (res.ok) { | ||||
|                 const json = await res.json(); | ||||
|                 if (json["data"]) { | ||||
|                     this.data = json["data"]; | ||||
|                 } | ||||
|                 document.getElementById("bank-submit-button").disabled = false; | ||||
|             } | ||||
|         } | ||||
|     }) | ||||
|  | ||||
|     Alpine.data('billing_infos', () => ({ | ||||
|         errors: [], | ||||
|         successful: false, | ||||
|         url: billing_info_exist ? edit_billing_info_url : create_billing_info_url, | ||||
|  | ||||
|         async send_form() { | ||||
|             const form = document.getElementById("billing_info_form"); | ||||
|             const submit_button = form.querySelector("input[type=submit]") | ||||
|             submit_button.disabled = true; | ||||
|             document.getElementById("bank-submit-button").disabled = true; | ||||
|             this.successful = false | ||||
|  | ||||
|             let payload = {}; | ||||
|             for (const elem of form.querySelectorAll("input")) { | ||||
|                 if (elem.type === "text" && elem.value) { | ||||
|                     payload[elem.name] = elem.value; | ||||
|                 } | ||||
|             } | ||||
|             const country = form.querySelector("select"); | ||||
|             if (country && country.value) { | ||||
|                 payload[country.name] = country.value; | ||||
|             } | ||||
|             const request = new Request(this.url, { | ||||
|                 method: "POST", | ||||
|                 headers: { | ||||
|                     'Accept': 'application/json', | ||||
|                     'Content-Type': 'application/json', | ||||
|                     'X-CSRFToken': getCSRFToken(), | ||||
|                 }, | ||||
|                 body: JSON.stringify(payload), | ||||
|             }); | ||||
|             const res = await fetch(request); | ||||
|             const json = await res.json(); | ||||
|             if (json["errors"]) { | ||||
|                 this.errors = json["errors"]; | ||||
|             } else { | ||||
|                 this.errors = []; | ||||
|                 this.successful = true; | ||||
|                 this.url = edit_billing_info_url; | ||||
|                 Alpine.store("billing_inputs").fill(); | ||||
|             } | ||||
|             submit_button.disabled = false; | ||||
|         } | ||||
|     })) | ||||
| }) | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user