mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-25 18:44:23 +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:
parent
99827e005b
commit
705b9b1e6a
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();
|
||||||
|
});
|
@ -5,16 +5,22 @@
|
|||||||
{{ counter }}
|
{{ counter }}
|
||||||
{% endblock %}
|
{% 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 %}
|
{% 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>
|
||||||
@ -28,7 +34,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="add_student_card">
|
<input type="hidden" name="action" value="add_student_card">
|
||||||
{% trans %}Add a student card{% endtrans %}
|
{% 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'] %}
|
{% if request.session['not_valid_student_card_uid'] %}
|
||||||
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
|
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -49,59 +55,67 @@
|
|||||||
<div id="click_form">
|
<div id="click_form">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<h5>{% trans %}Selling{% endtrans %}</h5>
|
||||||
<div>
|
<div>
|
||||||
|
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user.id) %}
|
||||||
|
|
||||||
{% raw %}
|
{# Formulaire pour rechercher un produit en tapant son code dans une barre de recherche #}
|
||||||
<div class="important">
|
<form method="post" action=""
|
||||||
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
class="code_form" @submit.prevent="handle_code">
|
||||||
</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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="code">
|
<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 %}"/>
|
<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>
|
<p>{% trans %}Basket: {% endtrans %}</p>
|
||||||
|
|
||||||
{% raw %}
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="p_info,p_id in basket">
|
<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">
|
<form method="post" action="" class="inline del_product_form"
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
@submit.prevent="handle_action">
|
||||||
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="del_product">
|
<input type="hidden" name="action" value="del_product">
|
||||||
<input type="hidden" name="product_id" v-bind:value="p_id">
|
<input type="hidden" name="product_id" :value="id">
|
||||||
<button type="submit"> - </button>
|
<input type="submit" value="-"/>
|
||||||
</form>
|
</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">
|
<form method="post" action="" class="inline add_product_form"
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
@submit.prevent="handle_action">
|
||||||
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="add_product">
|
<input type="hidden" name="action" value="add_product">
|
||||||
<input type="hidden" name="product_id" v-bind:value="p_id">
|
<input type="hidden" name="product_id" :value="id">
|
||||||
<button type="submit"> + </button>
|
<input type="submit" value="+">
|
||||||
</form>
|
</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>
|
<span x-text="products[id].name"></span> :
|
||||||
</li>
|
<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>
|
</ul>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
|
|
||||||
<div class="important">
|
<form method="post"
|
||||||
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
||||||
</div>
|
|
||||||
{% endraw %}
|
|
||||||
|
|
||||||
<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="finish">
|
||||||
<input type="submit" value="{% trans %}Finish{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Finish{% endtrans %}"/>
|
||||||
</form>
|
</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 %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="cancel">
|
<input type="hidden" name="action" value="cancel">
|
||||||
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
|
||||||
@ -110,7 +124,8 @@
|
|||||||
{% if (counter.type == 'BAR' and barmens_can_refill) %}
|
{% if (counter.type == 'BAR' and barmens_can_refill) %}
|
||||||
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
||||||
<div>
|
<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 %}
|
{% csrf_token %}
|
||||||
{{ refill_form.as_p() }}
|
{{ refill_form.as_p() }}
|
||||||
<input type="hidden" name="action" value="refill">
|
<input type="hidden" name="action" value="refill">
|
||||||
@ -130,95 +145,45 @@
|
|||||||
<div id="cat_{{ category|slugify }}">
|
<div id="cat_{{ category|slugify }}">
|
||||||
<h5>{{ category }}</h5>
|
<h5>{{ category }}</h5>
|
||||||
{% for p in categories[category] -%}
|
{% for p in categories[category] -%}
|
||||||
{% set file = None %}
|
<form method="post"
|
||||||
{% if p.icon %}
|
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"
|
||||||
{% set file = p.icon.url %}
|
class="form_button add_product_form" @submit.prevent="handle_action">
|
||||||
{% 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">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="add_product">
|
<input type="hidden" name="action" value="add_product">
|
||||||
<input type="hidden" name="product_id" value="{{ p.id }}">
|
<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>
|
</form>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$( function() {
|
const csrf_token = "{{ csrf_token }}";
|
||||||
/* Vue.JS dynamic form */
|
const click_api_url = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
|
||||||
const click_form_vue = Vue.createApp({
|
const basket = {{ request.session["basket"]|tojson }};
|
||||||
data() {
|
const products = {
|
||||||
return {
|
{%- for p in products -%}
|
||||||
js_csrf_token: "{{ csrf_token }}",
|
|
||||||
products: {
|
|
||||||
{% for p in products -%}
|
|
||||||
{{ p.id }}: {
|
{{ p.id }}: {
|
||||||
code: "{{ p.code }}",
|
code: "{{ p.code }}",
|
||||||
name: "{{ p.name }}",
|
name: "{{ p.name }}",
|
||||||
selling_price: "{{ p.selling_price }}",
|
price: {{ p.price }},
|
||||||
special_selling_price: "{{ p.special_selling_price }}",
|
|
||||||
},
|
},
|
||||||
{%- endfor %}
|
{%- endfor -%}
|
||||||
},
|
};
|
||||||
basket: {{ request.session["basket"]|tojson }},
|
const products_autocomplete = [
|
||||||
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 = [
|
|
||||||
{% for p in products -%}
|
{% for p in products -%}
|
||||||
{
|
{
|
||||||
value: "{{ p.code }}",
|
value: "{{ p.code }}",
|
||||||
@ -227,41 +192,5 @@ $( function() {
|
|||||||
},
|
},
|
||||||
{%- endfor %}
|
{%- 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>
|
</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)
|
||||||
|
@ -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,7 +673,8 @@ 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():
|
||||||
|
raise PermissionDenied
|
||||||
form = RefillForm(request.POST)
|
form = RefillForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.instance.counter = self.object
|
form.instance.counter = self.object
|
||||||
@ -674,13 +683,16 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
form.instance.save()
|
form.instance.save()
|
||||||
else:
|
else:
|
||||||
self.refill_form = form
|
self.refill_form = form
|
||||||
else:
|
|
||||||
raise PermissionDenied
|
|
||||||
|
|
||||||
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",
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user