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; |   text-decoration: underline; | ||||||
| } | } | ||||||
|  |  | ||||||
| #bar_ui { | #bar-ui { | ||||||
|     padding: 0.4em; |     padding: 0.4em; | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
|   | |||||||
| @@ -451,11 +451,11 @@ class Counter(models.Model): | |||||||
|         Show if the counter authorize the refilling with physic money |         Show if the counter authorize the refilling with physic money | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         if ( |         if self.type != "BAR": | ||||||
|             self.id in SITH_COUNTER_OFFICES |             return False | ||||||
|         ):  # If the counter is the counters 'AE' or 'BdF', the refiling are authorized |         if self.id in SITH_COUNTER_OFFICES: | ||||||
|  |             # If the counter is either 'AE' or 'BdF', refills are authorized | ||||||
|             return True |             return True | ||||||
|  |  | ||||||
|         is_ae_member = False |         is_ae_member = False | ||||||
|         ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"]) |         ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"]) | ||||||
|         for barman in self.get_barmen_list(): |         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(); | ||||||
|  | }); | ||||||
| @@ -2,266 +2,195 @@ | |||||||
| {% from "core/macros.jinja" import user_mini_profile, user_subscription %} | {% from "core/macros.jinja" import user_mini_profile, user_subscription %} | ||||||
|  |  | ||||||
| {% block title %} | {% block title %} | ||||||
| {{ counter }} |     {{ 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 %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block info_boxes %} | {% block info_boxes %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  |  | ||||||
| {% block nav %} | {% block nav %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
| <h4 id="click_interface">{{ counter }}</h4> |     <h4 id="click_interface">{{ counter }}</h4> | ||||||
|  |  | ||||||
| <div id="bar_ui"> |     <div id="bar-ui" x-data="counter"> | ||||||
|     <noscript> |         <noscript> | ||||||
|         <p class="important">Javascript is required for the counter UI.</p> |             <p class="important">Javascript is required for the counter UI.</p> | ||||||
|     </noscript> |         </noscript> | ||||||
|  |  | ||||||
|     <div id="user_info"> |  | ||||||
|         <h5>{% trans %}Customer{% endtrans %}</h5> |  | ||||||
|         {{ user_mini_profile(customer.user) }} |  | ||||||
|         {{ user_subscription(customer.user) }} |  | ||||||
|         <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> |  | ||||||
|         <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> |  | ||||||
|             {% csrf_token %} |  | ||||||
|             <input type="hidden" name="action" value="add_student_card"> |  | ||||||
|             {% trans %}Add a student card{% endtrans %} |  | ||||||
|             <input type="input" 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 %} |  | ||||||
|             <input type="submit" value="{% trans %}Go{% endtrans %}" /> |  | ||||||
|         </form> |  | ||||||
|         <h6>{% trans %}Registered cards{% endtrans %}</h6> |  | ||||||
|         {% if customer.student_cards.exists() %} |  | ||||||
|             <ul> |  | ||||||
|                 {% for card in customer.student_cards.all() %} |  | ||||||
|                     <li>{{ card.uid }}</li> |  | ||||||
|                 {% endfor %} |  | ||||||
|             </ul> |  | ||||||
|         {% else %} |  | ||||||
|             {% trans %}No card registered{% endtrans %} |  | ||||||
|         {% endif %} |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div id="click_form"> |  | ||||||
|         <h5>{% trans %}Selling{% endtrans %}</h5> |  | ||||||
|         <div> |  | ||||||
|  |  | ||||||
|             {% 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"> |  | ||||||
|                 {% csrf_token %} |  | ||||||
|                 <input type="hidden" name="action" value="code"> |  | ||||||
|                 <input type="input" name="code" value="" class="focus" id="code_field"/> |  | ||||||
|                 <input type="submit" value="{% trans %}Go{% endtrans %}" /> |  | ||||||
|             </form> |  | ||||||
|             <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"> |  | ||||||
|                         <input type="hidden" name="action" value="del_product"> |  | ||||||
|                         <input type="hidden" name="product_id" v-bind:value="p_id"> |  | ||||||
|                         <button type="submit"> - </button> |  | ||||||
|                     </form> |  | ||||||
|  |  | ||||||
|                     {{ p_info["qty"] + p_info["bonus_qty"] }} |  | ||||||
|  |  | ||||||
|                     <form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action"> |  | ||||||
|                         <input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_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> |  | ||||||
|                     </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> |  | ||||||
|             </ul> |  | ||||||
|             <p> |  | ||||||
|                 <strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong> |  | ||||||
|             </p> |  | ||||||
|  |  | ||||||
|             <div class="important"> |  | ||||||
|                 <p v-for="error in errors"><strong>{{ error }}</strong></p> |  | ||||||
|             </div> |  | ||||||
|             {% endraw %} |  | ||||||
|  |  | ||||||
|  |         <div id="user_info"> | ||||||
|  |             <h5>{% trans %}Customer{% endtrans %}</h5> | ||||||
|  |             {{ user_mini_profile(customer.user) }} | ||||||
|  |             {{ user_subscription(customer.user) }} | ||||||
|  |             <p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p> | ||||||
|             <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 %} |                 {% csrf_token %} | ||||||
|                 <input type="hidden" name="action" value="finish"> |                 <input type="hidden" name="action" value="add_student_card"> | ||||||
|                 <input type="submit" value="{% trans %}Finish{% endtrans %}" /> |                 {% trans %}Add a student card{% endtrans %} | ||||||
|             </form> |                 <input type="text" name="student_card_uid"/> | ||||||
|             <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> |                 {% if request.session['not_valid_student_card_uid'] %} | ||||||
|                 {% csrf_token %} |                     <p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p> | ||||||
|                 <input type="hidden" name="action" value="cancel"> |  | ||||||
|                 <input type="submit" value="{% trans %}Cancel{% endtrans %}" /> |  | ||||||
|             </form> |  | ||||||
|         </div> |  | ||||||
|         {% 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) }}"> |  | ||||||
|                 {% csrf_token %} |  | ||||||
|                 {{ refill_form.as_p() }} |  | ||||||
|                 <input type="hidden" name="action" value="refill"> |  | ||||||
|                 <input type="submit" value="{% trans %}Go{% endtrans %}" /> |  | ||||||
|             </form> |  | ||||||
|         </div> |  | ||||||
|         {% endif %} |  | ||||||
|     </div> |  | ||||||
|  |  | ||||||
|     <div id="products"> |  | ||||||
|         <ul> |  | ||||||
|             {% for category in categories.keys() -%} |  | ||||||
|             <li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li> |  | ||||||
|             {%- endfor %} |  | ||||||
|         </ul> |  | ||||||
|         {% for category in categories.keys() -%} |  | ||||||
|         <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 %} |                 {% 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"> |                 <input type="submit" value="{% trans %}Go{% endtrans %}"/> | ||||||
|  |             </form> | ||||||
|  |             <h6>{% trans %}Registered cards{% endtrans %}</h6> | ||||||
|  |             {% if customer.student_cards.exists() %} | ||||||
|  |                 <ul> | ||||||
|  |                     {% for card in customer.student_cards.all() %} | ||||||
|  |                         <li>{{ card.uid }}</li> | ||||||
|  |                     {% endfor %} | ||||||
|  |                 </ul> | ||||||
|  |             {% else %} | ||||||
|  |                 {% trans %}No card registered{% endtrans %} | ||||||
|  |             {% endif %} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <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) %} | ||||||
|  |  | ||||||
|  |                 {# 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 %} |                     {% csrf_token %} | ||||||
|                     <input type="hidden" name="action" value="add_product"> |                     <input type="hidden" name="action" value="code"> | ||||||
|                     <input type="hidden" name="product_id" value="{{ p.id }}"> |                     <label for="code_field"></label> | ||||||
|                     <button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button> |                     <input type="text" name="code" value="" class="focus" id="code_field"/> | ||||||
|  |                     <input type="submit" value="{% trans %}Go{% endtrans %}"/> | ||||||
|                 </form> |                 </form> | ||||||
|  |  | ||||||
|  |                 <template x-for="error in errors"> | ||||||
|  |                     <div class="alert alert-red" x-text="error"> | ||||||
|  |                     </div> | ||||||
|  |                 </template> | ||||||
|  |                 <p>{% trans %}Basket: {% endtrans %}</p> | ||||||
|  |  | ||||||
|  |                 <ul> | ||||||
|  |                     <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" :value="id"> | ||||||
|  |                                 <input type="submit" value="-"/> | ||||||
|  |                             </form> | ||||||
|  |  | ||||||
|  |                             <span x-text="item['qty'] + item['bonus_qty']"></span> | ||||||
|  |  | ||||||
|  |                             <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" :value="id"> | ||||||
|  |                                 <input type="submit" value="+"> | ||||||
|  |                             </form> | ||||||
|  |  | ||||||
|  |                             <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: </strong> | ||||||
|  |                     <strong x-text="sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 })"></strong> | ||||||
|  |                     <strong> €</strong> | ||||||
|  |                 </p> | ||||||
|  |  | ||||||
|  |                 <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) }}"> | ||||||
|  |                     {% csrf_token %} | ||||||
|  |                     <input type="hidden" name="action" value="cancel"> | ||||||
|  |                     <input type="submit" value="{% trans %}Cancel{% endtrans %}"/> | ||||||
|  |                 </form> | ||||||
|  |             </div> | ||||||
|  |             {% 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) }}"> | ||||||
|  |                         {% csrf_token %} | ||||||
|  |                         {{ refill_form.as_p() }} | ||||||
|  |                         <input type="hidden" name="action" value="refill"> | ||||||
|  |                         <input type="submit" value="{% trans %}Go{% endtrans %}"/> | ||||||
|  |                     </form> | ||||||
|  |                 </div> | ||||||
|  |             {% endif %} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div id="products"> | ||||||
|  |             <ul> | ||||||
|  |                 {% for category in categories.keys() -%} | ||||||
|  |                     <li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li> | ||||||
|  |                 {%- endfor %} | ||||||
|  |             </ul> | ||||||
|  |             {% for category in categories.keys() -%} | ||||||
|  |                 <div id="cat_{{ category|slugify }}"> | ||||||
|  |                     <h5>{{ category }}</h5> | ||||||
|  |                     {% for p in categories[category] -%} | ||||||
|  |                         <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> | ||||||
|  |                                 {% 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 %} |             {%- endfor %} | ||||||
|         </div> |         </div> | ||||||
|         {%- endfor %} |  | ||||||
|     </div> |     </div> | ||||||
| </div> |  | ||||||
|  |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block script %} | {% block script %} | ||||||
| {{ super() }} |     {{ super() }} | ||||||
| <script src="{{ static('core/js/vue.global.prod.js') }}"></script> |     <script> | ||||||
| <script> |         const csrf_token = "{{ csrf_token }}"; | ||||||
| $( function() { |         const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"; | ||||||
|     /* Vue.JS dynamic form */ |         const basket = {{ request.session["basket"]|tojson }}; | ||||||
|     const click_form_vue = Vue.createApp({ |         const products = { | ||||||
|         data() { |             {%- for p in products -%} | ||||||
|             return { |                 {{ p.id }}: { | ||||||
|                 js_csrf_token: "{{ csrf_token }}", |                     code: "{{ p.code }}", | ||||||
|                 products: { |                     name: "{{ p.name }}", | ||||||
|                     {% for p in products -%} |                     price: {{ p.price }}, | ||||||
|                       {{ p.id }}: { |                 }, | ||||||
|                           code: "{{ p.code }}", |             {%- endfor -%} | ||||||
|                           name: "{{ p.name }}", |         }; | ||||||
|                           selling_price: "{{ p.selling_price }}", |         const products_autocomplete = [ | ||||||
|                           special_selling_price: "{{ p.special_selling_price }}", |             {% for p in products -%} | ||||||
|                         }, |                 { | ||||||
|                     {%- endfor %} |                     value: "{{ p.code }}", | ||||||
|                   }, |                     label: "{{ p.name }}", | ||||||
|                 basket: {{ request.session["basket"]|tojson }}, |                     tags: "{{ p.code }} {{ p.name }}", | ||||||
|                 errors: [], |                 }, | ||||||
|               } |             {%- endfor %} | ||||||
|         }, |         ]; | ||||||
|         methods: { |     </script> | ||||||
|             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 = [ |  | ||||||
|     {% for p in products -%} |  | ||||||
|         { |  | ||||||
|             value: "{{ p.code }}", |  | ||||||
|             label: "{{ p.name }}", |  | ||||||
|             tags: "{{ p.code }} {{ p.name }}", |  | ||||||
|         }, |  | ||||||
|     {%- 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 %} | {% endblock %} | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ class CounterTest(TestCase): | |||||||
|         self.foyer = Counter.objects.get(id=2) |         self.foyer = Counter.objects.get(id=2) | ||||||
|  |  | ||||||
|     def test_full_click(self): |     def test_full_click(self): | ||||||
|         response = self.client.post( |         self.client.post( | ||||||
|             reverse("counter:login", kwargs={"counter_id": self.mde.id}), |             reverse("counter:login", kwargs={"counter_id": self.mde.id}), | ||||||
|             {"username": self.skia.username, "password": "plop"}, |             {"username": self.skia.username, "password": "plop"}, | ||||||
|         ) |         ) | ||||||
| @@ -62,13 +62,12 @@ class CounterTest(TestCase): | |||||||
|             reverse("counter:details", kwargs={"counter_id": self.mde.id}), |             reverse("counter:details", kwargs={"counter_id": self.mde.id}), | ||||||
|             {"code": "4000k", "counter_token": counter_token}, |             {"code": "4000k", "counter_token": counter_token}, | ||||||
|         ) |         ) | ||||||
|         location = response.get("location") |         counter_url = response.get("location") | ||||||
|  |  | ||||||
|         response = self.client.get(response.get("location")) |         response = self.client.get(response.get("location")) | ||||||
|         self.assertTrue(">Richard Batsbak</" in str(response.content)) |         self.assertTrue(">Richard Batsbak</" in str(response.content)) | ||||||
|  |  | ||||||
|         self.client.post( |         self.client.post( | ||||||
|             location, |             counter_url, | ||||||
|             { |             { | ||||||
|                 "action": "refill", |                 "action": "refill", | ||||||
|                 "amount": "5", |                 "amount": "5", | ||||||
| @@ -76,17 +75,27 @@ class CounterTest(TestCase): | |||||||
|                 "bank": "OTHER", |                 "bank": "OTHER", | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         self.client.post(location, {"action": "code", "code": "BARB"}) |         self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml") | ||||||
|         self.client.post(location, {"action": "add_product", "product_id": "4"}) |         self.client.post( | ||||||
|         self.client.post(location, {"action": "del_product", "product_id": "4"}) |             counter_url, "action=add_product&product_id=4", content_type="text/xml" | ||||||
|         self.client.post(location, {"action": "code", "code": "2xdeco"}) |         ) | ||||||
|         self.client.post(location, {"action": "code", "code": "1xbarb"}) |         self.client.post( | ||||||
|         response = self.client.post(location, {"action": "code", "code": "fin"}) |             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_get = self.client.get(response.get("location")) | ||||||
|         response_content = response_get.content.decode("utf-8") |         response_content = response_get.content.decode("utf-8") | ||||||
|         self.assertTrue("<li>2 x Barbar" in str(response_content)) |         self.assertTrue("2 x Barbar" in str(response_content)) | ||||||
|         self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content)) |         self.assertTrue("2 x Déconsigne Eco-cup" in str(response_content)) | ||||||
|         self.assertTrue( |         self.assertTrue( | ||||||
|             "<p>Client : Richard Batsbak - Nouveau montant : 3.60" |             "<p>Client : Richard Batsbak - Nouveau montant : 3.60" | ||||||
|             in str(response_content) |             in str(response_content) | ||||||
| @@ -98,7 +107,7 @@ class CounterTest(TestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             location, |             counter_url, | ||||||
|             { |             { | ||||||
|                 "action": "refill", |                 "action": "refill", | ||||||
|                 "amount": "5", |                 "amount": "5", | ||||||
| @@ -108,7 +117,7 @@ class CounterTest(TestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertTrue(response.status_code == 200) |         self.assertTrue(response.status_code == 200) | ||||||
|  |  | ||||||
|         response = self.client.post( |         self.client.post( | ||||||
|             reverse("counter:login", kwargs={"counter_id": self.foyer.id}), |             reverse("counter:login", kwargs={"counter_id": self.foyer.id}), | ||||||
|             {"username": self.krophil.username, "password": "plop"}, |             {"username": self.krophil.username, "password": "plop"}, | ||||||
|         ) |         ) | ||||||
| @@ -125,10 +134,10 @@ class CounterTest(TestCase): | |||||||
|             reverse("counter:details", kwargs={"counter_id": self.foyer.id}), |             reverse("counter:details", kwargs={"counter_id": self.foyer.id}), | ||||||
|             {"code": "4000k", "counter_token": counter_token}, |             {"code": "4000k", "counter_token": counter_token}, | ||||||
|         ) |         ) | ||||||
|         location = response.get("location") |         counter_url = response.get("location") | ||||||
|  |  | ||||||
|         response = self.client.post( |         response = self.client.post( | ||||||
|             location, |             counter_url, | ||||||
|             { |             { | ||||||
|                 "action": "refill", |                 "action": "refill", | ||||||
|                 "amount": "5", |                 "amount": "5", | ||||||
| @@ -144,7 +153,7 @@ class CounterStatsTest(TestCase): | |||||||
|         call_command("populate") |         call_command("populate") | ||||||
|         self.counter = Counter.objects.filter(id=2).first() |         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 |         # Test with not login user | ||||||
|         response = self.client.get(reverse("counter:stats", args=[self.counter.id])) |         response = self.client.get(reverse("counter:stats", args=[self.counter.id])) | ||||||
|         self.assertTrue(response.status_code == 403) |         self.assertTrue(response.status_code == 403) | ||||||
|   | |||||||
							
								
								
									
										100
									
								
								counter/views.py
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								counter/views.py
									
									
									
									
									
								
							| @@ -22,8 +22,10 @@ | |||||||
| # | # | ||||||
| # | # | ||||||
| import json | import json | ||||||
|  | from urllib.parse import parse_qs | ||||||
|  |  | ||||||
| from django.contrib.auth.decorators import login_required | from django.contrib.auth.decorators import login_required | ||||||
|  | from django.db.models import F | ||||||
| from django.shortcuts import get_object_or_404 | from django.shortcuts import get_object_or_404 | ||||||
| from django.http import Http404 | from django.http import Http404 | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| @@ -300,7 +302,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|     current_tab = "counter" |     current_tab = "counter" | ||||||
|  |  | ||||||
|     def render_to_response(self, *args, **kwargs): |     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": []} |             response = {"errors": []} | ||||||
|             status = HTTPStatus.OK |             status = HTTPStatus.OK | ||||||
|  |  | ||||||
| @@ -395,42 +397,40 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|         request.session["not_valid_student_card_uid"] = False |         request.session["not_valid_student_card_uid"] = False | ||||||
|         if self.object.type != "BAR": |         if self.object.type != "BAR": | ||||||
|             self.operator = request.user |             self.operator = request.user | ||||||
|         elif self.is_barman_price(): |         elif self.customer_is_barman(): | ||||||
|             self.operator = self.customer.user |             self.operator = self.customer.user | ||||||
|         else: |         else: | ||||||
|             self.operator = self.object.get_random_barman() |             self.operator = self.object.get_random_barman() | ||||||
|  |         action = self.request.POST.get("action", None) | ||||||
|         if "add_product" in request.POST["action"]: |         if action is None: | ||||||
|  |             action = parse_qs(request.body.decode()).get("action", [""])[0] | ||||||
|  |         if action == "add_product": | ||||||
|             self.add_product(request) |             self.add_product(request) | ||||||
|         elif "add_student_card" in request.POST["action"]: |         elif action == "add_student_card": | ||||||
|             self.add_student_card(request) |             self.add_student_card(request) | ||||||
|         elif "del_product" in request.POST["action"]: |         elif action == "del_product": | ||||||
|             self.del_product(request) |             self.del_product(request) | ||||||
|         elif "refill" in request.POST["action"]: |         elif action == "refill": | ||||||
|             self.refill(request) |             self.refill(request) | ||||||
|         elif "code" in request.POST["action"]: |         elif action == "code": | ||||||
|             return self.parse_code(request) |             return self.parse_code(request) | ||||||
|         elif "cancel" in request.POST["action"]: |         elif action == "cancel": | ||||||
|             return self.cancel(request) |             return self.cancel(request) | ||||||
|         elif "finish" in request.POST["action"]: |         elif action == "finish": | ||||||
|             return self.finish(request) |             return self.finish(request) | ||||||
|         context = self.get_context_data(object=self.object) |         context = self.get_context_data(object=self.object) | ||||||
|         return self.render_to_response(context) |         return self.render_to_response(context) | ||||||
|  |  | ||||||
|     def is_barman_price(self): |     def customer_is_barman(self) -> bool: | ||||||
|         if self.object.type == "BAR" and self.customer.user.id in [ |         barmen = self.object.barmen_list | ||||||
|             s.id for s in self.object.get_barmen_list() |         return self.object.type == "BAR" and self.customer.user in barmen | ||||||
|         ]: |  | ||||||
|             return True |  | ||||||
|         else: |  | ||||||
|             return False |  | ||||||
|  |  | ||||||
|     def get_product(self, pid): |     def get_product(self, pid): | ||||||
|         return Product.objects.filter(pk=int(pid)).first() |         return Product.objects.filter(pk=int(pid)).first() | ||||||
|  |  | ||||||
|     def get_price(self, pid): |     def get_price(self, pid): | ||||||
|         p = self.get_product(pid) |         p = self.get_product(pid) | ||||||
|         if self.is_barman_price(): |         if self.customer_is_barman(): | ||||||
|             price = p.special_selling_price |             price = p.special_selling_price | ||||||
|         else: |         else: | ||||||
|             price = p.selling_price |             price = p.selling_price | ||||||
| @@ -475,13 +475,22 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|             self.compute_record_product(request, product) |             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): |     def add_product(self, request, q=1, p=None): | ||||||
|         """ |         """ | ||||||
|         Add a product to the basket |         Add a product to the basket | ||||||
|         q is the quantity passed as integer |         q is the quantity passed as integer | ||||||
|         p is the product id, passed as an 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) |         pid = str(pid) | ||||||
|         price = self.get_price(pid) |         price = self.get_price(pid) | ||||||
|         total = self.sum_basket(request) |         total = self.sum_basket(request) | ||||||
| @@ -563,7 +572,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|  |  | ||||||
|     def del_product(self, request): |     def del_product(self, request): | ||||||
|         """Delete a product from the basket""" |         """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) |         product = self.get_product(pid) | ||||||
|         if pid in request.session["basket"]: |         if pid in request.session["basket"]: | ||||||
|             if ( |             if ( | ||||||
| @@ -576,30 +585,29 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|                 request.session["basket"][pid]["qty"] -= 1 |                 request.session["basket"][pid]["qty"] -= 1 | ||||||
|             if request.session["basket"][pid]["qty"] <= 0: |             if request.session["basket"][pid]["qty"] <= 0: | ||||||
|                 del request.session["basket"][pid] |                 del request.session["basket"][pid] | ||||||
|         else: |  | ||||||
|             request.session["basket"][pid] = None |  | ||||||
|         request.session.modified = True |         request.session.modified = True | ||||||
|  |  | ||||||
|     def parse_code(self, request): |     def parse_code(self, request): | ||||||
|         """Parse the string entered by the barman""" |         """ | ||||||
|         string = str(request.POST["code"]).upper() |         Parse the string entered by the barman | ||||||
|         if string == _("END"): |         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) |             return self.finish(request) | ||||||
|         elif string == _("CAN"): |         elif string == "ANN": | ||||||
|             return self.cancel(request) |             return self.cancel(request) | ||||||
|         regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$") |         regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$") | ||||||
|         m = regex.match(string) |         m = regex.match(string) | ||||||
|         if m is not None: |         if m is not None: | ||||||
|             nb = m.group("nb") |             nb = m.group("nb") | ||||||
|             code = m.group("code") |             code = m.group("code") | ||||||
|             if nb is None: |             nb = int(nb) if nb is not None else 1 | ||||||
|                 nb = 1 |  | ||||||
|             else: |  | ||||||
|                 nb = int(nb) |  | ||||||
|             p = self.object.products.filter(code=code).first() |             p = self.object.products.filter(code=code).first() | ||||||
|             if p is not None: |             if p is not None: | ||||||
|                 while nb > 0 and not self.add_product(request, nb, p.id): |                 self.add_product(request, nb, p.id) | ||||||
|                     nb -= 1 |  | ||||||
|         context = self.get_context_data(object=self.object) |         context = self.get_context_data(object=self.object) | ||||||
|         return self.render_to_response(context) |         return self.render_to_response(context) | ||||||
|  |  | ||||||
| @@ -613,7 +621,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|             for pid, infos in request.session["basket"].items(): |             for pid, infos in request.session["basket"].items(): | ||||||
|                 # This duplicates code for DB optimization (prevent to load many times the same object) |                 # This duplicates code for DB optimization (prevent to load many times the same object) | ||||||
|                 p = Product.objects.filter(pk=pid).first() |                 p = Product.objects.filter(pk=pid).first() | ||||||
|                 if self.is_barman_price(): |                 if self.customer_is_barman(): | ||||||
|                     uprice = p.special_selling_price |                     uprice = p.special_selling_price | ||||||
|                 else: |                 else: | ||||||
|                     uprice = p.selling_price |                     uprice = p.selling_price | ||||||
| @@ -665,22 +673,26 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): | |||||||
|  |  | ||||||
|     def refill(self, request): |     def refill(self, request): | ||||||
|         """Refill the customer's account""" |         """Refill the customer's account""" | ||||||
|         if self.get_object().type == "BAR" and self.object.can_refill(): |         if not self.object.can_refill(): | ||||||
|             form = RefillForm(request.POST) |  | ||||||
|             if form.is_valid(): |  | ||||||
|                 form.instance.counter = self.object |  | ||||||
|                 form.instance.operator = self.operator |  | ||||||
|                 form.instance.customer = self.customer |  | ||||||
|                 form.instance.save() |  | ||||||
|             else: |  | ||||||
|                 self.refill_form = form |  | ||||||
|         else: |  | ||||||
|             raise PermissionDenied |             raise PermissionDenied | ||||||
|  |         form = RefillForm(request.POST) | ||||||
|  |         if form.is_valid(): | ||||||
|  |             form.instance.counter = self.object | ||||||
|  |             form.instance.operator = self.operator | ||||||
|  |             form.instance.customer = self.customer | ||||||
|  |             form.instance.save() | ||||||
|  |         else: | ||||||
|  |             self.refill_form = form | ||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs): |     def get_context_data(self, **kwargs): | ||||||
|         """Add customer to the context""" |         """Add customer to the context""" | ||||||
|         kwargs = super(CounterClick, self).get_context_data(**kwargs) |         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"] = {} |         kwargs["categories"] = {} | ||||||
|         for product in kwargs["products"]: |         for product in kwargs["products"]: | ||||||
|             if product.product_type: |             if product.product_type: | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ | |||||||
|                                             <i class="fa fa-2x fa-picture-o product-image" ></i> |                                             <i class="fa fa-2x fa-picture-o product-image" ></i> | ||||||
|                                         {% endif %} |                                         {% endif %} | ||||||
|                                         <div class="product-description"> |                                         <div class="product-description"> | ||||||
|                                             <h4>{{ p.name }}</strong></h4> |                                             <h4>{{ p.name }}</h4> | ||||||
|                                             <p>{{ p.selling_price }} €</p> |                                             <p>{{ p.selling_price }} €</p> | ||||||
|                                         </div> |                                         </div> | ||||||
|                                     </button> |                                     </button> | ||||||
|   | |||||||
| @@ -689,6 +689,5 @@ SITH_FRONT_DEP_VERSIONS = { | |||||||
|     "https://github.com/viralpatel/jquery.shorten/": "", |     "https://github.com/viralpatel/jquery.shorten/": "", | ||||||
|     "https://github.com/getsentry/sentry-javascript/": "4.0.6", |     "https://github.com/getsentry/sentry-javascript/": "4.0.6", | ||||||
|     "https://github.com/jhuckaby/webcamjs/": "1.0.0", |     "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", |     "https://github.com/alpinejs/alpine": "3.10.5", | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user