mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-04 02:53:06 +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();
 | 
			
		||||
});
 | 
			
		||||
@@ -2,19 +2,25 @@
 | 
			
		||||
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
 | 
			
		||||
 | 
			
		||||
{% 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 %}
 | 
			
		||||
 | 
			
		||||
{% block info_boxes %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block nav %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% 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>
 | 
			
		||||
            <p class="important">Javascript is required for the counter UI.</p>
 | 
			
		||||
        </noscript>
 | 
			
		||||
@@ -28,11 +34,11 @@
 | 
			
		||||
                {% 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 %}
 | 
			
		||||
            <input type="submit" value="{% trans %}Go{% endtrans %}" />
 | 
			
		||||
                <input type="submit" value="{% trans %}Go{% endtrans %}"/>
 | 
			
		||||
            </form>
 | 
			
		||||
            <h6>{% trans %}Registered cards{% endtrans %}</h6>
 | 
			
		||||
            {% if customer.student_cards.exists() %}
 | 
			
		||||
@@ -49,72 +55,81 @@
 | 
			
		||||
        <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"/>
 | 
			
		||||
                <input type="submit" value="{% trans %}Go{% endtrans %}" />
 | 
			
		||||
                    <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 %}" />
 | 
			
		||||
                    <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 %}" />
 | 
			
		||||
                    <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) }}">
 | 
			
		||||
                    <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 %}" />
 | 
			
		||||
                        <input type="submit" value="{% trans %}Go{% endtrans %}"/>
 | 
			
		||||
                    </form>
 | 
			
		||||
                </div>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
    </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 -%}
 | 
			
		||||
    {{ super() }}
 | 
			
		||||
    <script>
 | 
			
		||||
        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>
 | 
			
		||||
    </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