mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 14:13:21 +00:00
counter: make click page dynamic to avoid repetitive loading
This makes the whole click page load only once for a normal click workflow. The current basket is now rendered client side with Vue.JS, and the backend view is able to answer with JSON if asked to. This should lighten the workflow a lot on the client side, especially with poor connectivity, and the server should also feel lighter during big events, due to far less complex Jinja pages to render.
This commit is contained in:
parent
efb70652af
commit
406380e4f1
1
core/static/core/js/vue.global.prod.js
Normal file
1
core/static/core/js/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
@ -1125,7 +1125,7 @@ u, .underline {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bar_ui {
|
#click_form {
|
||||||
float: left;
|
float: left;
|
||||||
min-width: 57%;
|
min-width: 57%;
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,6 @@
|
|||||||
{% extends "core/base.jinja" %}
|
{% extends "core/base.jinja" %}
|
||||||
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
{% from "core/macros.jinja" import user_mini_profile, user_subscription %}
|
||||||
|
|
||||||
|
|
||||||
{% macro add_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="add_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro del_product(id, content, class="") %}
|
|
||||||
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="action" value="del_product">
|
|
||||||
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
|
|
||||||
</form>
|
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ counter }}
|
{{ counter }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -58,56 +41,57 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="bar_ui">
|
<div id="bar_ui">
|
||||||
|
<noscript>
|
||||||
|
<p class="important">Javascript is required for the counter UI.</p>
|
||||||
|
</noscript>
|
||||||
|
<div id="click_form">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<h5>{% trans %}Selling{% endtrans %}</h5>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="important">
|
<div class="important">
|
||||||
{% if request.session['too_young'] %}
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_allowed'] %}
|
|
||||||
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['no_age'] %}
|
|
||||||
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_enough'] %}
|
|
||||||
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
</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) }}" 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"/>
|
<input type="input" name="code" value="" class="focus" id="code_field"/>
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||||
</form>
|
</form>
|
||||||
<p>{% trans %}Basket: {% endtrans %}</p>
|
<p>{% trans %}Basket: {% endtrans %}</p>
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for id,infos in request.session['basket']|dictsort %}
|
<li v-for="p_info,p_id in basket">
|
||||||
{% set product = counter.products.filter(id=id).first() %}
|
|
||||||
{% set s = infos['qty'] * infos['price'] / 100 %}
|
<form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
|
||||||
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }}
|
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
|
||||||
{{ product.name }}: {{ "%0.2f"|format(s) }} €
|
<input type="hidden" name="action" value="del_product">
|
||||||
{% if infos['bonus_qty'] %}
|
<input type="hidden" name="product_id" v-bind:value="p_id">
|
||||||
P
|
<button type="submit"> - </button>
|
||||||
{% endif %}
|
</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>
|
</li>
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
</ul>
|
||||||
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p>
|
<p>
|
||||||
|
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
|
||||||
|
</p>
|
||||||
|
{% endraw %}
|
||||||
|
|
||||||
<div class="important">
|
<div class="important">
|
||||||
{% if request.session['too_young'] %}
|
<p v-for="error in errors"><strong>{{ error }}</strong></p>
|
||||||
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_allowed'] %}
|
|
||||||
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['no_age'] %}
|
|
||||||
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if request.session['not_enough'] %}
|
|
||||||
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
|
|
||||||
{% endif %}
|
|
||||||
</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 %}
|
||||||
<input type="hidden" name="action" value="finish">
|
<input type="hidden" name="action" value="finish">
|
||||||
@ -130,8 +114,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="products">
|
<div id="products">
|
||||||
<ul>
|
<ul>
|
||||||
{% for category in categories.keys() -%}
|
{% for category in categories.keys() -%}
|
||||||
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
<li><a href="#cat_{{ category|slugify }}">{{ category }}</a></li>
|
||||||
@ -147,19 +131,87 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set file = static('core/img/na.gif') %}
|
{% set file = static('core/img/na.gif') %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %}
|
<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">
|
||||||
{{ add_product(p.id, prod, "form_button") }}
|
{% 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>
|
||||||
|
</form>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
$( function() {
|
$( function() {
|
||||||
|
/* Vue.JS dynamic form */
|
||||||
|
const click_form_vue = Vue.createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
js_csrf_token: "{{ csrf_token }}",
|
||||||
|
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 }}",
|
||||||
|
},
|
||||||
|
{%- 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 */
|
/* Autocompletion in the code field */
|
||||||
var products_autocomplete = [
|
var products_autocomplete = [
|
||||||
{% for p in products -%}
|
{% for p in products -%}
|
||||||
@ -196,7 +248,7 @@ $( function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Accordion UI between basket and refills */
|
/* Accordion UI between basket and refills */
|
||||||
$("#bar_ui").accordion({
|
$("#click_form").accordion({
|
||||||
heightStyle: "content",
|
heightStyle: "content",
|
||||||
activate: function(event, ui){
|
activate: function(event, ui){
|
||||||
$(".focus").focus();
|
$(".focus").focus();
|
||||||
|
@ -68,18 +68,31 @@ class CounterTest(TestCase):
|
|||||||
location,
|
location,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
"action": "refill",
|
||||||
"amount": "10",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
response = self.client.post(location, {"action": "code", "code": "BARB"})
|
||||||
|
response = self.client.post(location, {"action": "add_product", "product_id": "4"})
|
||||||
|
response = self.client.post(location, {"action": "del_product", "product_id": "4"})
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
|
||||||
|
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
|
||||||
response = self.client.post(location, {"action": "code", "code": "fin"})
|
response = self.client.post(location, {"action": "code", "code": "fin"})
|
||||||
|
|
||||||
response_get = self.client.get(response.get("location"))
|
response_get = self.client.get(response.get("location"))
|
||||||
|
response_content = response_get.content.decode("utf-8")
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
"<p>Client : Richard Batsbak - Nouveau montant : 8.30"
|
"<li>2 x Barbar"
|
||||||
in str(response_get.content)
|
in str(response_content)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
"<li>2 x Déconsigne Eco-cup"
|
||||||
|
in str(response_content)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
"<p>Client : Richard Batsbak - Nouveau montant : 3.60"
|
||||||
|
in str(response_content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ from django.views.generic.edit import (
|
|||||||
from django.forms.models import modelform_factory
|
from django.forms.models import modelform_factory
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy, reverse
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
|
|||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
from datetime import date, timedelta, datetime
|
from datetime import date, timedelta, datetime
|
||||||
|
from http import HTTPStatus
|
||||||
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
|
||||||
from ajax_select import make_ajax_field
|
from ajax_select import make_ajax_field
|
||||||
|
|
||||||
@ -357,6 +358,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
pk_url_kwarg = "counter_id"
|
pk_url_kwarg = "counter_id"
|
||||||
current_tab = "counter"
|
current_tab = "counter"
|
||||||
|
|
||||||
|
def render_to_response(self, *args, **kwargs):
|
||||||
|
if self.request.is_ajax(): # JSON response for AJAX requests
|
||||||
|
response = {"errors": []}
|
||||||
|
status = HTTPStatus.OK
|
||||||
|
|
||||||
|
if self.request.session["too_young"]:
|
||||||
|
response["errors"].append(_("Too young for that product"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_allowed"]:
|
||||||
|
response["errors"].append(_("Not allowed for that product"))
|
||||||
|
status = HTTPStatus.FORBIDDEN
|
||||||
|
if self.request.session["no_age"]:
|
||||||
|
response["errors"].append(_("No date of birth provided"))
|
||||||
|
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
|
||||||
|
if self.request.session["not_enough"]:
|
||||||
|
response["errors"].append(_("Not enough money"))
|
||||||
|
status = HTTPStatus.PAYMENT_REQUIRED
|
||||||
|
|
||||||
|
if len(response["errors"]) > 1:
|
||||||
|
status = HTTPStatus.BAD_REQUEST
|
||||||
|
|
||||||
|
response["basket"] = self.request.session["basket"]
|
||||||
|
|
||||||
|
return JsonResponse(response, status=status)
|
||||||
|
|
||||||
|
else: # Standard HTML page
|
||||||
|
return super().render_to_response(*args, **kwargs)
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
Loading…
Reference in New Issue
Block a user