mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 17:13:08 +00:00 
			
		
		
		
	Passage de vue à Alpine pour les comptoirs (#561)
Vue, c'est cool, mais avec Django c'est un peu chiant à utiliser. Alpine a l'avantage d'être plus léger et d'avoir une syntaxe qui ne ressemble pas à celle de Jinja (ce qui évite d'avoir à mettre des {% raw %} partout).
			
			
This commit is contained in:
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1224,7 +1224,7 @@ u, .underline { | ||||
|   text-decoration: underline; | ||||
| } | ||||
|  | ||||
| #bar_ui { | ||||
| #bar-ui { | ||||
|     padding: 0.4em; | ||||
|     display: flex; | ||||
|     flex-wrap: wrap; | ||||
|   | ||||
| @@ -451,11 +451,11 @@ class Counter(models.Model): | ||||
|         Show if the counter authorize the refilling with physic money | ||||
|         """ | ||||
|  | ||||
|         if ( | ||||
|             self.id in SITH_COUNTER_OFFICES | ||||
|         ):  # If the counter is the counters 'AE' or 'BdF', the refiling are authorized | ||||
|         if self.type != "BAR": | ||||
|             return False | ||||
|         if self.id in SITH_COUNTER_OFFICES: | ||||
|             # If the counter is either 'AE' or 'BdF', refills are authorized | ||||
|             return True | ||||
|  | ||||
|         is_ae_member = False | ||||
|         ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"]) | ||||
|         for barman in self.get_barmen_list(): | ||||
|   | ||||
							
								
								
									
										78
									
								
								counter/static/counter/js/counter_click.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								counter/static/counter/js/counter_click.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| document.addEventListener('alpine:init', () => { | ||||
|     Alpine.data('counter', () => ({ | ||||
|         basket: basket, | ||||
|         errors: [], | ||||
|  | ||||
|         sum_basket() { | ||||
|             if (!this.basket || Object.keys(this.basket).length === 0) { | ||||
|                 return 0; | ||||
|             } | ||||
|             const total = Object.values(this.basket) | ||||
|                 .reduce((acc, cur) => acc + cur["qty"] * cur["price"], 0); | ||||
|             return total / 100; | ||||
|         }, | ||||
|  | ||||
|         async handle_code(event) { | ||||
|             const code = $(event.target).find("#code_field").val().toUpperCase(); | ||||
|             if(["FIN", "ANN"].includes(code)) { | ||||
|                 $(event.target).submit(); | ||||
|             } else { | ||||
|                 await this.handle_action(event); | ||||
|             } | ||||
|         }, | ||||
|  | ||||
|         async handle_action(event) { | ||||
|             const payload = $(event.target).serialize(); | ||||
|             let request = new Request(click_api_url, { | ||||
|                 method: "POST", | ||||
|                 body: payload, | ||||
|                 headers: { | ||||
|                     'Accept': 'application/json', | ||||
|                     'X-CSRFToken': csrf_token, | ||||
|                 } | ||||
|             }) | ||||
|             const response = await fetch(request); | ||||
|             const json = await response.json(); | ||||
|             this.basket = json["basket"] | ||||
|             this.errors = json["errors"] | ||||
|             $('form.code_form #code_field').val("").focus(); | ||||
|         } | ||||
|     })) | ||||
| }) | ||||
|  | ||||
| $(function () { | ||||
|     /* Autocompletion in the code field */ | ||||
|     const code_field = $("#code_field"); | ||||
|  | ||||
|     let quantity = ""; | ||||
|     let search = ""; | ||||
|     code_field.autocomplete({ | ||||
|         select: function (event, ui) { | ||||
|             event.preventDefault(); | ||||
|             code_field.val(quantity + ui.item.value); | ||||
|         }, | ||||
|         focus: function (event, ui) { | ||||
|             event.preventDefault(); | ||||
|             code_field.val(quantity + ui.item.value); | ||||
|         }, | ||||
|         source: function (request, response) { | ||||
|             // by the dark magic of JS, parseInt("123abc") === 123 | ||||
|             quantity = parseInt(request.term); | ||||
|             search = request.term.slice(quantity.toString().length) | ||||
|             let matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i"); | ||||
|             response($.grep(products_autocomplete, function (value) { | ||||
|                 value = value.tags; | ||||
|                 return matcher.test(value); | ||||
|             })); | ||||
|         }, | ||||
|     }); | ||||
|  | ||||
|     /* Accordion UI between basket and refills */ | ||||
|     $("#click_form").accordion({ | ||||
|         heightStyle: "content", | ||||
|         activate: () => $(".focus").focus(), | ||||
|     }); | ||||
|     $("#products").tabs(); | ||||
|  | ||||
|     code_field.focus(); | ||||
| }); | ||||
| @@ -5,16 +5,22 @@ | ||||
|     {{ counter }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block additional_js %} | ||||
|     <script src="{{ static('counter/js/counter_click.js') }}" defer></script> | ||||
|     <script src="{{ static('core/js/alpinejs.min.js') }}" defer></script> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block info_boxes %} | ||||
| {% endblock %} | ||||
|  | ||||
|  | ||||
| {% block nav %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|     <h4 id="click_interface">{{ counter }}</h4> | ||||
|  | ||||
| <div id="bar_ui"> | ||||
|     <div id="bar-ui" x-data="counter"> | ||||
|         <noscript> | ||||
|             <p class="important">Javascript is required for the counter UI.</p> | ||||
|         </noscript> | ||||
| @@ -28,7 +34,7 @@ | ||||
|                 {% csrf_token %} | ||||
|                 <input type="hidden" name="action" value="add_student_card"> | ||||
|                 {% trans %}Add a student card{% endtrans %} | ||||
|             <input type="input" name="student_card_uid" /> | ||||
|                 <input type="text" name="student_card_uid"/> | ||||
|                 {% if request.session['not_valid_student_card_uid'] %} | ||||
|                     <p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p> | ||||
|                 {% endif %} | ||||
| @@ -49,59 +55,67 @@ | ||||
|         <div id="click_form"> | ||||
|             <h5>{% trans %}Selling{% endtrans %}</h5> | ||||
|             <div> | ||||
|                 {% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %} | ||||
|  | ||||
|             {% raw %} | ||||
|             <div class="important"> | ||||
|                 <p v-for="error in errors"><strong>{{ error }}</strong></p> | ||||
|             </div> | ||||
|             {% endraw %} | ||||
|  | ||||
|             <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code"> | ||||
|                 {# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #} | ||||
|                 <form method="post" action="" | ||||
|                       class="code_form" @submit.prevent="handle_code"> | ||||
|                     {% csrf_token %} | ||||
|                     <input type="hidden" name="action" value="code"> | ||||
|                 <input type="input" name="code" value="" class="focus" id="code_field"/> | ||||
|                     <label for="code_field"></label> | ||||
|                     <input type="text" name="code" value="" class="focus" id="code_field"/> | ||||
|                     <input type="submit" value="{% trans %}Go{% endtrans %}"/> | ||||
|                 </form> | ||||
|  | ||||
|                 <template x-for="error in errors"> | ||||
|                     <div class="alert alert-red" x-text="error"> | ||||
|                     </div> | ||||
|                 </template> | ||||
|                 <p>{% trans %}Basket: {% endtrans %}</p> | ||||
|  | ||||
|             {% raw %} | ||||
|                 <ul> | ||||
|                 <li v-for="p_info,p_id in basket"> | ||||
|  | ||||
|                     <form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action"> | ||||
|                         <input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token"> | ||||
|                     <template x-for="[id, item] in Object.entries(basket)" :key="id"> | ||||
|                         <div> | ||||
|                             <form method="post" action="" class="inline del_product_form" | ||||
|                                   @submit.prevent="handle_action"> | ||||
|                                 {% csrf_token %} | ||||
|                                 <input type="hidden" name="action" value="del_product"> | ||||
|                         <input type="hidden" name="product_id" v-bind:value="p_id"> | ||||
|                         <button type="submit"> - </button> | ||||
|                                 <input type="hidden" name="product_id" :value="id"> | ||||
|                                 <input type="submit" value="-"/> | ||||
|                             </form> | ||||
|  | ||||
|                     {{ p_info["qty"] + p_info["bonus_qty"] }} | ||||
|                             <span x-text="item['qty'] + item['bonus_qty']"></span> | ||||
|  | ||||
|                     <form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action"> | ||||
|                         <input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token"> | ||||
|                             <form method="post" action="" class="inline add_product_form" | ||||
|                                   @submit.prevent="handle_action"> | ||||
|                                 {% csrf_token %} | ||||
|                                 <input type="hidden" name="action" value="add_product"> | ||||
|                         <input type="hidden" name="product_id" v-bind:value="p_id"> | ||||
|                         <button type="submit"> + </button> | ||||
|                                 <input type="hidden" name="product_id" :value="id"> | ||||
|                                 <input type="submit" value="+"> | ||||
|                             </form> | ||||
|  | ||||
|                     {{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span> | ||||
|                 </li> | ||||
|                             <span x-text="products[id].name"></span> : | ||||
|                             <span x-text="(item['qty'] * item['price'] / 100) | ||||
|                                           .toLocaleString(undefined, { minimumFractionDigits: 2 })"> | ||||
|                             </span> € | ||||
|                             <template x-if="item['bonus_qty'] > 0">P</template> | ||||
|                         </div> | ||||
|                     </template> | ||||
|                 </ul> | ||||
|                 <p> | ||||
|                 <strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong> | ||||
|                     <strong>Total: </strong> | ||||
|                     <strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong> | ||||
|                     <strong> €</strong> | ||||
|                 </p> | ||||
|  | ||||
|             <div class="important"> | ||||
|                 <p v-for="error in errors"><strong>{{ error }}</strong></p> | ||||
|             </div> | ||||
|             {% endraw %} | ||||
|  | ||||
|             <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                 <form method="post" | ||||
|                       action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                     {% csrf_token %} | ||||
|                     <input type="hidden" name="action" value="finish"> | ||||
|                     <input type="submit" value="{% trans %}Finish{% endtrans %}"/> | ||||
|                 </form> | ||||
|             <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                 <form method="post" | ||||
|                       action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                     {% csrf_token %} | ||||
|                     <input type="hidden" name="action" value="cancel"> | ||||
|                     <input type="submit" value="{% trans %}Cancel{% endtrans %}"/> | ||||
| @@ -110,7 +124,8 @@ | ||||
|             {% if (counter.type == 'BAR' and barmens_can_refill) %} | ||||
|                 <h5>{% trans %}Refilling{% endtrans %}</h5> | ||||
|                 <div> | ||||
|             <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                     <form method="post" | ||||
|                           action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> | ||||
|                         {% csrf_token %} | ||||
|                         {{ refill_form.as_p() }} | ||||
|                         <input type="hidden" name="action" value="refill"> | ||||
| @@ -130,95 +145,45 @@ | ||||
|                 <div id="cat_{{ category|slugify }}"> | ||||
|                     <h5>{{ category }}</h5> | ||||
|                     {% for p in categories[category] -%} | ||||
|                 {% set file = None %} | ||||
|                 {% if p.icon %} | ||||
|                     {% set file = p.icon.url %} | ||||
|                 {% else %} | ||||
|                     {% set file = static('core/img/na.gif') %} | ||||
|                 {% endif %} | ||||
|                 <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action"> | ||||
|                         <form method="post" | ||||
|                               action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" | ||||
|                               class="form_button add_product_form" @submit.prevent="handle_action"> | ||||
|                             {% csrf_token %} | ||||
|                             <input type="hidden" name="action" value="add_product"> | ||||
|                             <input type="hidden" name="product_id" value="{{ p.id }}"> | ||||
|                     <button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button> | ||||
|                             <button type="submit"> | ||||
|                                 <strong>{{ p.name }}</strong> | ||||
|                                 {% if p.icon %} | ||||
|                                     <img src="{{ p.icon.url }}" alt="image de {{ p.name }}"/> | ||||
|                                 {% else %} | ||||
|                                     <img src="{{ static('core/img/na.gif') }}" alt="image de {{ p.name }}"/> | ||||
|                                 {% endif %} | ||||
|                                 <span>{{ p.price }} €<br>{{ p.code }}</span> | ||||
|                             </button> | ||||
|                         </form> | ||||
|                     {%- endfor %} | ||||
|                 </div> | ||||
|             {%- endfor %} | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| {% endblock %} | ||||
|  | ||||
| {% block script %} | ||||
|     {{ super() }} | ||||
| <script src="{{ static('core/js/vue.global.prod.js') }}"></script> | ||||
|     <script> | ||||
| $( function() { | ||||
|     /* Vue.JS dynamic form */ | ||||
|     const click_form_vue = Vue.createApp({ | ||||
|         data() { | ||||
|             return { | ||||
|                 js_csrf_token: "{{ csrf_token }}", | ||||
|                 products: { | ||||
|                     {% for p in products -%} | ||||
|         const csrf_token = "{{ csrf_token }}"; | ||||
|         const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"; | ||||
|         const basket = {{ request.session["basket"]|tojson }}; | ||||
|         const products = { | ||||
|             {%- for p in products -%} | ||||
|                 {{ p.id }}: { | ||||
|                     code: "{{ p.code }}", | ||||
|                     name: "{{ p.name }}", | ||||
|                           selling_price: "{{ p.selling_price }}", | ||||
|                           special_selling_price: "{{ p.special_selling_price }}", | ||||
|                     price: {{ p.price }}, | ||||
|                 }, | ||||
|                     {%- endfor %} | ||||
|                   }, | ||||
|                 basket: {{ request.session["basket"]|tojson }}, | ||||
|                 errors: [], | ||||
|               } | ||||
|         }, | ||||
|         methods: { | ||||
|             sum_basket() { | ||||
|                 var vm = this; | ||||
|                 var total = 0; | ||||
|                 for(idx in vm.basket) { | ||||
|                     var item = vm.basket[idx]; | ||||
|                     console.log(item); | ||||
|                     total += item["qty"] * item["price"]; | ||||
|                 } | ||||
|                 return total / 100; | ||||
|             }, | ||||
|             handle_code(event) { | ||||
|                 var vm = this; | ||||
|                 var code = $(event.target).find("#code_field").val().toUpperCase(); | ||||
|                 console.log("Code:"); | ||||
|                 console.log(code); | ||||
|                 if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") { | ||||
|                     $(event.target).submit(); | ||||
|                 } else { | ||||
|                     vm.handle_action(event); | ||||
|                 } | ||||
|             }, | ||||
|             handle_action(event) { | ||||
|                 var vm = this; | ||||
|                 var payload = $(event.target).serialize(); | ||||
|                 $.ajax({ | ||||
|                     type: 'post', | ||||
|                     dataType: 'json', | ||||
|                     data: payload, | ||||
|                     success: function(response) { | ||||
|                         vm.basket = response.basket; | ||||
|                         vm.errors = []; | ||||
|                     }, | ||||
|                     error: function(error) { | ||||
|                         vm.basket = error.responseJSON.basket; | ||||
|                         vm.errors = error.responseJSON.errors; | ||||
|                     } | ||||
|                 }); | ||||
|                 $('form.code_form #code_field').val("").focus(); | ||||
|               } | ||||
|           } | ||||
|       }).mount('#bar_ui'); | ||||
|  | ||||
|     /* Autocompletion in the code field */ | ||||
|     var products_autocomplete = [ | ||||
|             {%- endfor -%} | ||||
|         }; | ||||
|         const products_autocomplete = [ | ||||
|             {% for p in products -%} | ||||
|                 { | ||||
|                     value: "{{ p.code }}", | ||||
| @@ -227,41 +192,5 @@ $( function() { | ||||
|                 }, | ||||
|             {%- endfor %} | ||||
|         ]; | ||||
|  | ||||
|     var quantity = ""; | ||||
|     var search = ""; | ||||
|     var pattern = /^(\d+x)?(.*)/i; | ||||
|     $( "#code_field" ).autocomplete({ | ||||
|         select: function (event, ui) { | ||||
|             event.preventDefault(); | ||||
|             $("#code_field").val(quantity + ui.item.value); | ||||
|         }, | ||||
|         focus: function (event, ui) { | ||||
|             event.preventDefault(); | ||||
|             $("#code_field").val(quantity + ui.item.value); | ||||
|         }, | ||||
|         source: function( request, response ) { | ||||
|             var res = pattern.exec(request.term); | ||||
|             quantity = res[1] || ""; | ||||
|             search = res[2]; | ||||
|             var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" ); | ||||
|             response($.grep( products_autocomplete, function( value ) { | ||||
|                 value = value.tags; | ||||
|                 return matcher.test( value ); | ||||
|             })); | ||||
|         }, | ||||
|     }); | ||||
|  | ||||
|     /* Accordion UI between basket and refills */ | ||||
|     $("#click_form").accordion({ | ||||
|         heightStyle: "content", | ||||
|         activate: function(event, ui){ | ||||
|             $(".focus").focus(); | ||||
|         } | ||||
|         }); | ||||
|     $("#products").tabs(); | ||||
|  | ||||
|     $("#code_field").focus(); | ||||
| }); | ||||
|     </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -42,7 +42,7 @@ class CounterTest(TestCase): | ||||
|         self.foyer = Counter.objects.get(id=2) | ||||
|  | ||||
|     def test_full_click(self): | ||||
|         response = self.client.post( | ||||
|         self.client.post( | ||||
|             reverse("counter:login", kwargs={"counter_id": self.mde.id}), | ||||
|             {"username": self.skia.username, "password": "plop"}, | ||||
|         ) | ||||
| @@ -62,13 +62,12 @@ class CounterTest(TestCase): | ||||
|             reverse("counter:details", kwargs={"counter_id": self.mde.id}), | ||||
|             {"code": "4000k", "counter_token": counter_token}, | ||||
|         ) | ||||
|         location = response.get("location") | ||||
|  | ||||
|         counter_url = response.get("location") | ||||
|         response = self.client.get(response.get("location")) | ||||
|         self.assertTrue(">Richard Batsbak</" in str(response.content)) | ||||
|  | ||||
|         self.client.post( | ||||
|             location, | ||||
|             counter_url, | ||||
|             { | ||||
|                 "action": "refill", | ||||
|                 "amount": "5", | ||||
| @@ -76,17 +75,27 @@ class CounterTest(TestCase): | ||||
|                 "bank": "OTHER", | ||||
|             }, | ||||
|         ) | ||||
|         self.client.post(location, {"action": "code", "code": "BARB"}) | ||||
|         self.client.post(location, {"action": "add_product", "product_id": "4"}) | ||||
|         self.client.post(location, {"action": "del_product", "product_id": "4"}) | ||||
|         self.client.post(location, {"action": "code", "code": "2xdeco"}) | ||||
|         self.client.post(location, {"action": "code", "code": "1xbarb"}) | ||||
|         response = self.client.post(location, {"action": "code", "code": "fin"}) | ||||
|         self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml") | ||||
|         self.client.post( | ||||
|             counter_url, "action=add_product&product_id=4", content_type="text/xml" | ||||
|         ) | ||||
|         self.client.post( | ||||
|             counter_url, "action=del_product&product_id=4", content_type="text/xml" | ||||
|         ) | ||||
|         self.client.post( | ||||
|             counter_url, "action=code&code=2xdeco", content_type="text/xml" | ||||
|         ) | ||||
|         self.client.post( | ||||
|             counter_url, "action=code&code=1xbarb", content_type="text/xml" | ||||
|         ) | ||||
|         response = self.client.post( | ||||
|             counter_url, "action=code&code=fin", content_type="text/xml" | ||||
|         ) | ||||
|  | ||||
|         response_get = self.client.get(response.get("location")) | ||||
|         response_content = response_get.content.decode("utf-8") | ||||
|         self.assertTrue("<li>2 x Barbar" in str(response_content)) | ||||
|         self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content)) | ||||
|         self.assertTrue("2 x Barbar" in str(response_content)) | ||||
|         self.assertTrue("2 x Déconsigne Eco-cup" in str(response_content)) | ||||
|         self.assertTrue( | ||||
|             "<p>Client : Richard Batsbak - Nouveau montant : 3.60" | ||||
|             in str(response_content) | ||||
| @@ -98,7 +107,7 @@ class CounterTest(TestCase): | ||||
|         ) | ||||
|  | ||||
|         response = self.client.post( | ||||
|             location, | ||||
|             counter_url, | ||||
|             { | ||||
|                 "action": "refill", | ||||
|                 "amount": "5", | ||||
| @@ -108,7 +117,7 @@ class CounterTest(TestCase): | ||||
|         ) | ||||
|         self.assertTrue(response.status_code == 200) | ||||
|  | ||||
|         response = self.client.post( | ||||
|         self.client.post( | ||||
|             reverse("counter:login", kwargs={"counter_id": self.foyer.id}), | ||||
|             {"username": self.krophil.username, "password": "plop"}, | ||||
|         ) | ||||
| @@ -125,10 +134,10 @@ class CounterTest(TestCase): | ||||
|             reverse("counter:details", kwargs={"counter_id": self.foyer.id}), | ||||
|             {"code": "4000k", "counter_token": counter_token}, | ||||
|         ) | ||||
|         location = response.get("location") | ||||
|         counter_url = response.get("location") | ||||
|  | ||||
|         response = self.client.post( | ||||
|             location, | ||||
|             counter_url, | ||||
|             { | ||||
|                 "action": "refill", | ||||
|                 "amount": "5", | ||||
| @@ -144,7 +153,7 @@ class CounterStatsTest(TestCase): | ||||
|         call_command("populate") | ||||
|         self.counter = Counter.objects.filter(id=2).first() | ||||
|  | ||||
|     def test_unothorized_user_fail(self): | ||||
|     def test_unauthorised_user_fail(self): | ||||
|         # Test with not login user | ||||
|         response = self.client.get(reverse("counter:stats", args=[self.counter.id])) | ||||
|         self.assertTrue(response.status_code == 403) | ||||
|   | ||||
| @@ -22,8 +22,10 @@ | ||||
| # | ||||
| # | ||||
| import json | ||||
| from urllib.parse import parse_qs | ||||
|  | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django.db.models import F | ||||
| from django.shortcuts import get_object_or_404 | ||||
| from django.http import Http404 | ||||
| from django.core.exceptions import PermissionDenied | ||||
| @@ -300,7 +302,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|     current_tab = "counter" | ||||
|  | ||||
|     def render_to_response(self, *args, **kwargs): | ||||
|         if self.request.is_ajax():  # JSON response for AJAX requests | ||||
|         if self.is_ajax(self.request): | ||||
|             response = {"errors": []} | ||||
|             status = HTTPStatus.OK | ||||
|  | ||||
| @@ -395,42 +397,40 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|         request.session["not_valid_student_card_uid"] = False | ||||
|         if self.object.type != "BAR": | ||||
|             self.operator = request.user | ||||
|         elif self.is_barman_price(): | ||||
|         elif self.customer_is_barman(): | ||||
|             self.operator = self.customer.user | ||||
|         else: | ||||
|             self.operator = self.object.get_random_barman() | ||||
|  | ||||
|         if "add_product" in request.POST["action"]: | ||||
|         action = self.request.POST.get("action", None) | ||||
|         if action is None: | ||||
|             action = parse_qs(request.body.decode()).get("action", [""])[0] | ||||
|         if action == "add_product": | ||||
|             self.add_product(request) | ||||
|         elif "add_student_card" in request.POST["action"]: | ||||
|         elif action == "add_student_card": | ||||
|             self.add_student_card(request) | ||||
|         elif "del_product" in request.POST["action"]: | ||||
|         elif action == "del_product": | ||||
|             self.del_product(request) | ||||
|         elif "refill" in request.POST["action"]: | ||||
|         elif action == "refill": | ||||
|             self.refill(request) | ||||
|         elif "code" in request.POST["action"]: | ||||
|         elif action == "code": | ||||
|             return self.parse_code(request) | ||||
|         elif "cancel" in request.POST["action"]: | ||||
|         elif action == "cancel": | ||||
|             return self.cancel(request) | ||||
|         elif "finish" in request.POST["action"]: | ||||
|         elif action == "finish": | ||||
|             return self.finish(request) | ||||
|         context = self.get_context_data(object=self.object) | ||||
|         return self.render_to_response(context) | ||||
|  | ||||
|     def is_barman_price(self): | ||||
|         if self.object.type == "BAR" and self.customer.user.id in [ | ||||
|             s.id for s in self.object.get_barmen_list() | ||||
|         ]: | ||||
|             return True | ||||
|         else: | ||||
|             return False | ||||
|     def customer_is_barman(self) -> bool: | ||||
|         barmen = self.object.barmen_list | ||||
|         return self.object.type == "BAR" and self.customer.user in barmen | ||||
|  | ||||
|     def get_product(self, pid): | ||||
|         return Product.objects.filter(pk=int(pid)).first() | ||||
|  | ||||
|     def get_price(self, pid): | ||||
|         p = self.get_product(pid) | ||||
|         if self.is_barman_price(): | ||||
|         if self.customer_is_barman(): | ||||
|             price = p.special_selling_price | ||||
|         else: | ||||
|             price = p.selling_price | ||||
| @@ -475,13 +475,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|             self.compute_record_product(request, product) | ||||
|         ) | ||||
|  | ||||
|     @staticmethod | ||||
|     def is_ajax(request): | ||||
|         # when using the fetch API, the django request.POST dict is empty | ||||
|         # this is but a wretched contrivance which strive to replace | ||||
|         # the deprecated django is_ajax() method | ||||
|         # and which must be replaced as soon as possible | ||||
|         # by a proper separation between the api endpoints of the counter | ||||
|         return len(request.POST) == 0 and len(request.body) != 0 | ||||
|  | ||||
|     def add_product(self, request, q=1, p=None): | ||||
|         """ | ||||
|         Add a product to the basket | ||||
|         q is the quantity passed as integer | ||||
|         p is the product id, passed as an integer | ||||
|         """ | ||||
|         pid = p or request.POST["product_id"] | ||||
|         pid = p or parse_qs(request.body.decode())["product_id"][0] | ||||
|         pid = str(pid) | ||||
|         price = self.get_price(pid) | ||||
|         total = self.sum_basket(request) | ||||
| @@ -563,7 +572,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|     def del_product(self, request): | ||||
|         """Delete a product from the basket""" | ||||
|         pid = str(request.POST["product_id"]) | ||||
|         pid = parse_qs(request.body.decode())["product_id"][0] | ||||
|         product = self.get_product(pid) | ||||
|         if pid in request.session["basket"]: | ||||
|             if ( | ||||
| @@ -576,30 +585,29 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|                 request.session["basket"][pid]["qty"] -= 1 | ||||
|             if request.session["basket"][pid]["qty"] <= 0: | ||||
|                 del request.session["basket"][pid] | ||||
|         else: | ||||
|             request.session["basket"][pid] = None | ||||
|         request.session.modified = True | ||||
|  | ||||
|     def parse_code(self, request): | ||||
|         """Parse the string entered by the barman""" | ||||
|         string = str(request.POST["code"]).upper() | ||||
|         if string == _("END"): | ||||
|         """ | ||||
|         Parse the string entered by the barman | ||||
|         This can be of two forms : | ||||
|             - <str>, where the string is the code of the product | ||||
|             - <int>X<str>, where the integer is the quantity and str the code | ||||
|         """ | ||||
|         string = parse_qs(request.body.decode())["code"][0].upper() | ||||
|         if string == "FIN": | ||||
|             return self.finish(request) | ||||
|         elif string == _("CAN"): | ||||
|         elif string == "ANN": | ||||
|             return self.cancel(request) | ||||
|         regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$") | ||||
|         m = regex.match(string) | ||||
|         if m is not None: | ||||
|             nb = m.group("nb") | ||||
|             code = m.group("code") | ||||
|             if nb is None: | ||||
|                 nb = 1 | ||||
|             else: | ||||
|                 nb = int(nb) | ||||
|             nb = int(nb) if nb is not None else 1 | ||||
|             p = self.object.products.filter(code=code).first() | ||||
|             if p is not None: | ||||
|                 while nb > 0 and not self.add_product(request, nb, p.id): | ||||
|                     nb -= 1 | ||||
|                 self.add_product(request, nb, p.id) | ||||
|         context = self.get_context_data(object=self.object) | ||||
|         return self.render_to_response(context) | ||||
|  | ||||
| @@ -613,7 +621,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|             for pid, infos in request.session["basket"].items(): | ||||
|                 # This duplicates code for DB optimization (prevent to load many times the same object) | ||||
|                 p = Product.objects.filter(pk=pid).first() | ||||
|                 if self.is_barman_price(): | ||||
|                 if self.customer_is_barman(): | ||||
|                     uprice = p.special_selling_price | ||||
|                 else: | ||||
|                     uprice = p.selling_price | ||||
| @@ -665,7 +673,8 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|  | ||||
|     def refill(self, request): | ||||
|         """Refill the customer's account""" | ||||
|         if self.get_object().type == "BAR" and self.object.can_refill(): | ||||
|         if not self.object.can_refill(): | ||||
|             raise PermissionDenied | ||||
|         form = RefillForm(request.POST) | ||||
|         if form.is_valid(): | ||||
|             form.instance.counter = self.object | ||||
| @@ -674,13 +683,16 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | ||||
|             form.instance.save() | ||||
|         else: | ||||
|             self.refill_form = form | ||||
|         else: | ||||
|             raise PermissionDenied | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """Add customer to the context""" | ||||
|         kwargs = super(CounterClick, self).get_context_data(**kwargs) | ||||
|         kwargs["products"] = self.object.products.select_related("product_type") | ||||
|         products = self.object.products.select_related("product_type") | ||||
|         if self.customer_is_barman(): | ||||
|             products = products.annotate(price=F("special_selling_price")) | ||||
|         else: | ||||
|             products = products.annotate(price=F("selling_price")) | ||||
|         kwargs["products"] = products | ||||
|         kwargs["categories"] = {} | ||||
|         for product in kwargs["products"]: | ||||
|             if product.product_type: | ||||
|   | ||||
| @@ -111,7 +111,7 @@ | ||||
|                                             <i class="fa fa-2x fa-picture-o product-image" ></i> | ||||
|                                         {% endif %} | ||||
|                                         <div class="product-description"> | ||||
|                                             <h4>{{ p.name }}</strong></h4> | ||||
|                                             <h4>{{ p.name }}</h4> | ||||
|                                             <p>{{ p.selling_price }} €</p> | ||||
|                                         </div> | ||||
|                                     </button> | ||||
|   | ||||
| @@ -689,6 +689,5 @@ SITH_FRONT_DEP_VERSIONS = { | ||||
|     "https://github.com/viralpatel/jquery.shorten/": "", | ||||
|     "https://github.com/getsentry/sentry-javascript/": "4.0.6", | ||||
|     "https://github.com/jhuckaby/webcamjs/": "1.0.0", | ||||
|     "https://github.com/vuejs/vue-next": "3.2.18", | ||||
|     "https://github.com/alpinejs/alpine": "3.10.5", | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user