diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 0020b636..ee5376af 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -153,13 +153,28 @@ class APIModelObject extends ModelObject { */ static get url_model() {} + /** + * Request url to create an instance. + * @abstract + * @type {string} + */ + static url_create() {} + + /** + * Request url to edit an instance of this model. + * @abstract + * @param {*} api_pk - Identifier of a model instance. + * @type {string} + */ + static url_update_for(api_pk) {} + /** * Request url to get a single model instance data. * @abstract - * @param {*} api_pk - Identifier of a model instance in api requests. + * @param {*} api_pk - Identifier of a model instance. * @return {string} */ - static url_object_for(api_pk) {} + static url_read_for(api_pk) {} /** * See {@link Models.ModelObject|new ModelObject(data)}. @@ -181,36 +196,26 @@ class APIModelObject extends ModelObject { /** * Request url used to get current instance data. */ - get url_object() { + get url_read() { if (this._url_object === undefined) - return this.is_empty() ? '' : this.constructor.url_object_for(this.api_pk); + return this.is_empty() ? '' : this.constructor.url_read_for(this.api_pk); return this._url_object; } - set url_object(v) { this._url_object = v; } + set url_read(v) { this._url_object = v; } /** * Get data of a distant model instance. It sends a GET HTTP request to - * {@link Models.APIModelObject#url_object}. + * {@link Models.APIModelObject#url_read}. * @param {object} [api_options] Additional data appended to the request. - * @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds. - * @param {jQueryAjaxError} [on_error] A function to be called if the request fails. */ - fromAPI(api_options, on_success, on_error) { - var that = this; - + fromAPI(api_options) { api_options = api_options || {}; - on_success = on_success || $.noop; - on_error = on_error || $.noop; api_options['format'] = 'json'; - $.getJSON(this.url_object, api_options) - .done(function (json, textStatus, jqXHR) { - that.from(json); - on_success(json, textStatus, jqXHR); - }) - .fail(on_error); + return $.getJSON(this.url_read, api_options) + .done( (json) => this.from(json) ); } /** @@ -221,9 +226,9 @@ class APIModelObject extends ModelObject { * @param {jQueryAjaxError} [on_error] * @see {@link Models.APIModelObject#fromAPI|fromAPI} */ - get_by_apipk(api_pk, api_options, on_success, on_error) { - this.url_object = this.constructor.url_object_for(api_pk); - this.fromAPI(api_options, on_success, on_error); + get_by_apipk(api_pk, api_options) { + this.url_read = this.constructor.url_read_for(api_pk); + return this.fromAPI(api_options); } /** @@ -283,16 +288,30 @@ class Account extends APIModelObject { */ static get url_model() { return Urls['kfet.account'](); } + static url_create(trigramme) { + var url = Urls['kfet.account.create'](); + if (trigramme) { + var trigramme_url = encodeURIComponent(trigramme); + url += `?trigramme=${trigramme_url}` + } + return url; + } + /** * @default django-js-reverse('kfet.account.read')(trigramme) * @param {string} trigramme - * @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for} + * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for} */ - static url_object_for(trigramme) { + static url_read_for(trigramme) { var trigramme_url = encodeURIComponent(trigramme); return Urls['kfet.account.read'](trigramme_url); } + static url_update_for(trigramme) { + var trigramme_url = encodeURIComponent(trigramme); + return Urls['kfet.account.update'](trigramme_url); + } + /** * @default this.trigramme */ @@ -313,7 +332,7 @@ class Account extends APIModelObject { /** * Balance converted to UKF according to cof status. */ - get balance_ukf() { return amountToUKF(this.balance, this.is_cof); } + get balance_ukf() { return amountToUKF(this.balance, this.is_cof, true); } } @@ -355,10 +374,14 @@ class Checkout extends APIModelObject { /** * @default django-js-reverse('kfet.kpsul.checkout_data.read')(pk) * @param {string} api_pk - a checkout id - * @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for} + * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for} */ - static url_object_for(api_pk) { - return Urls['kfet.kpsul.checkout_data.read'](api_pk); + static url_read_for(api_pk) { + return Urls['kfet.checkout.read'](api_pk); + } + + static url_update_for(api_pk) { + return Urls['kfet.checkout.update'](api_pk); } /** @@ -404,6 +427,10 @@ class Statement extends ModelObject { }; } + static url_create(checkout_pk) { + return Urls['kfet.checkoutstatement.create'](checkout_pk); + } + /** * @default {@link Formatters.StatementFormatter} */ @@ -508,6 +535,10 @@ class Article extends ModelObject { // API currently returns a string object (serialization of Decimal type within Django) get price() { return this._price; } set price(v) { this._price = floatCheck(v); } + + is_low_stock(nb) { + return (-5 <= this.stock - nb && this.stock - nb <= 5); + } } /** @@ -1040,29 +1071,14 @@ class APIModelForest extends ModelForest { * Fills the instance with distant data. It sends a GET HTTP request to * {@link Models.APIModelForest#url_model}. * @param {object} [api_options] Additional data appended to the request. - * @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds. - * @param {jQueryAjaxError} [on_error] A function to be called if the request fails. */ - fromAPI(api_options, on_success, on_error) { - var that = this; - - api_options = api_options || {} - on_success = on_success || $.noop; - on_error = on_error || $.noop; + fromAPI(api_options) { + api_options = api_options || {}; api_options['format'] = 'json'; - $.ajax({ - dataType: "json", - url : this.constructor.url_model, - method : "POST", - data : api_options, - }) - .done(function(json, textStatus, jqXHR) { - that.from(json); - on_success(json, textStatus, jqXHR); - }) - .fail(on_error); + return $.getJSON(this.constructor.url_model, api_options) + .done( (json) => this.from(json) ); } } diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index c1807165..d9e2624c 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -70,6 +70,29 @@ function booleanCheck(v) { } +/** + * Short: Equivalent to python str format. + * Source: [MDN]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals}. + * @example + * var t1Closure = template`${0}${1}${0}!`; + * t1Closure('Y', 'A'); // "YAY!" + * @example + * var t2Closure = template`${0} ${'foo'}!`; + * t2Closure('Hello', {foo: 'World'}); // "Hello World!" + */ +function template(strings, ...keys) { + return (function(...values) { + var dict = values[values.length - 1] || {}; + var result = [strings[0]]; + keys.forEach(function(key, i) { + var value = Number.isInteger(key) ? values[key] : dict[key]; + result.push(value, strings[i + 1]); + }); + return result.join(''); + }); +} + + /** * Get and store K-Psul config from API. *

diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index eade395c..81ee2341 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -1,3 +1,10 @@ +/** + * @file K-Psul JS + * @copyright 2017 cof-geek + * @license MIT + */ + + class KPsulManager { constructor(env) { @@ -12,12 +19,25 @@ class KPsulManager { soft = soft || false; this.account_manager.reset(); + this.article_manager.reset(); if (!soft) { this.checkout_manager.reset(); + this.article_manager.reset_data(); } } + focus() { + if (this.checkout_manager.is_empty()) + this.checkout_manager.focus(); + else if (this.account_manager.is_empty()) + this.account_manager.focus(); + else + this.article_manager.focus(); + + return this; + } + } @@ -29,6 +49,15 @@ class AccountManager { this.account = new Account(); this.selection = new AccountSelection(this); this.search = new AccountSearch(this); + + // buttons: search, read or create + this._$buttons_container = this._$container.find('.buttons'); + this._buttons_templates = { + create: template``, + read: template``, + search: template``, + } + } is_empty() { return this.account.is_empty(); } @@ -45,25 +74,22 @@ class AccountManager { } _display_buttons() { - // dirty - - var buttons = ''; + var buttons; if (this.is_empty()) { var trigramme = this.selection.get(); if (trigramme.isValidTri()) { - var url_base = Urls['kfet.account.create'](); - var url = url_base + '?trigramme=' + encodeURIComponent(trigramme); - buttons += ''; + var url = Account.url_create(trigramme); + buttons = this._buttons_templates['create']({url: url}); } else { /* trigramme input is empty or invalid */ - buttons += ''; + buttons = this._buttons_templates['search'](); } } else { /* an account is loaded */ - var url = this.account.url_object; - buttons += ''; + var url = this.account.url_read; + buttons = this._buttons_templates['read']({url: url}); } - this._$container.find('.buttons').html(buttons); + this._$buttons_container.html(buttons); } update(trigramme) { @@ -73,10 +99,9 @@ class AccountManager { trigramme = trigramme || this.selection.get(); if (trigramme.isValidTri()) { - this.account.get_by_apipk(trigramme, {}, - () => this._update_on_success(), - () => this.reset_data() - ); + this.account.get_by_apipk(trigramme) + .done( () => this._update_on_success() ) + .fail( () => this.reset_data() ); } else { this.reset_data(); } @@ -86,7 +111,7 @@ class AccountManager { $('#id_on_acc').val(this.account.id); this.display(); - kpsul._env.articleSelect.focus(); + kpsul.focus(); kpsul._env.updateBasketAmount(); kpsul._env.updateBasketRel(); } @@ -250,6 +275,12 @@ class CheckoutManager { this._$laststatement_container = $('#last_statement'); this.laststatement = new Statement(); this.laststatement_display_prefix = '#checkout-last_statement_'; + + this._$buttons_container = this._$container.find('.buttons'); + this._buttons_templates = { + read: template``, + statement_create: template``, + } } update(id) { @@ -262,15 +293,11 @@ class CheckoutManager { 'last_statement': true, }; - this.checkout.get_by_apipk(id, api_options, - (data) => this._update_on_success(data), - () => this.reset_data()); + this.checkout.get_by_apipk(id, api_options) + .done( (data) => this._update_on_success(data) ) + .fail( () => this.reset_data() ); - if (kpsul.account_manager.is_empty()) { - kpsul.account_manager.focus(); - } else { - kpsul._env.articleSelect.focus().select(); - } + kpsul.focus(); } _update_on_success(data) { @@ -310,13 +337,13 @@ class CheckoutManager { _display_buttons() { var buttons = ''; if (!this.is_empty()) { - var id = this.checkout.id; - var url_details = Urls['kfet.checkout.read'](id); - var url_newcheckout = Urls['kfet.checkoutstatement.create'](id); - buttons += ''; - buttons += ''; + var url_newcheckout = Statement.url_create(this.checkout.id); + buttons += this._buttons_templates['statement_create']({ + url: url_newcheckout}); + var url_read = this.checkout.url_read; + buttons += this._buttons_templates['read']({url: url_read}); } - this._$container.find('.buttons').html(buttons); + this._$buttons_container.html(buttons); } reset() { @@ -335,6 +362,11 @@ class CheckoutManager { this.display(); } + + focus() { + this.selection.focus(); + return this; + } } @@ -367,6 +399,11 @@ class CheckoutSelection { reset() { this._$input.find('option:first').prop('selected', true); } + + focus() { + this._$input.focus(); + return this; + } } class ArticleManager { @@ -415,19 +452,23 @@ class ArticleManager { reset_data() { this._$container.html(''); this.list.clear(); - this.list.fromAPI({}, this.display_list.bind(this), $.noop) ; + this.list.fromAPI() + .done( () => this.display_list() ); + } + + get_article(id) { + return this.list.find('article', id).content; } - //TODO: filter articles before ? update_data(data) { for (let article_dict of data.articles) { - var article = this.list.find('article', article_dict['id']); + var article = this.get_article(article_dict.id); // For now, article additions are disregarded if (article) { - article.stock = article_dict['stock']; - this._$container.find('#article-'+article_dict['id']+' .stock') - .text(article_dict['stock']); + article.stock = article_dict.stock; + this._$container.find('#article-'+article.id+' .stock') + .text(article.stock); } } } @@ -480,7 +521,11 @@ class ArticleManager { } focus() { - this._$input.focus(); + if (this.is_empty()) + this._$input.focus(); + else + this._$nb.focus(); + return this; } diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index e8bc3c39..2b98838e 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -188,7 +188,7 @@ $(document).ready(function() { commentDialog.open({ callback: confirm_callback, - next_focus: articleSelect + next_focus: kpsul, }); } @@ -219,7 +219,7 @@ $(document).ready(function() { else displayErrors(getErrorsHtml(response)); }, - next_focus: articleSelect, + next_focus: kpsul, }); } @@ -228,15 +228,6 @@ $(document).ready(function() { performOperations(); }); - // ----- - // Articles data - // ----- - - var articleSelect = $('#article_autocomplete'); - var articleId = $('#article_id'); - var articleNb = $('#article_number'); - var articlesList = []; - // ----- // Basket // ----- @@ -276,17 +267,13 @@ $(document).ready(function() { .find('.name').text(article.name).end() .find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof)); basket_container.prepend(article_basket_html); - if (is_low_stock(article, nb)) + if (article.is_low_stock(nb)) article_basket_html.find('.lowstock') .show(); updateBasketRel(); } } - function is_low_stock(article, nb) { - return (-5 <= article.stock - nb && article.stock - nb <= 5); - } - function addDeposit(amount) { var deposit_basket_html = $(item_basket_default_html); var amount = parseFloat(amount).toFixed(2); @@ -419,15 +406,12 @@ $(document).ready(function() { function addExistingPurchase(opeindex, nb) { var type = formset_container.find("#id_form-"+opeindex+"-type").val(); var id = formset_container.find("#id_form-"+opeindex+"-article").val(); + var article = kpsul.article_manager.get_article(parseInt(id)); var nb_before = formset_container.find("#id_form-"+opeindex+"-article_nb").val(); var nb_after = parseInt(nb_before) + parseInt(nb); - var amountEuro_after = amountEuroPurchase(id, nb_after); + var amountEuro_after = amountEuroPurchase(article, nb_after); var amountUKF_after = amountToUKF(amountEuro_after, kpsul.account_manager.account.is_cof, false); - var i = 0; - while (i 0) { var article_html = basket_container.find('[data-opeindex='+opeindex+']'); article_html.find('.amount').text(amountUKF_after).end() - .find('.number').text('('+nb_after+'/'+article_data[4]+')').end() ; + .find('.number').text('('+nb_after+'/'+article.stock+')').end() ; } else { article_html = $(item_basket_default_html); article_html .attr('data-opeindex', opeindex) - .find('.number').text('('+nb_after+'/'+article_data[4]+')').end() - .find('.name').text(article_data[0]).end() + .find('.number').text('('+nb_after+'/'+article.stock+')').end() + .find('.name').text(article.name).end() .find('.amount').text(amountUKF_after); basket_container.prepend(article_basket_html); } - if (is_low_stock(id, nb_after)) + if (article.is_low_stock(nb_after)) article_html.find('.lowstock') .show(); else @@ -485,11 +469,9 @@ $(document).ready(function() { addDeposit(amount); } - var next_focus = articleSelect.val() ? articleNb : articleSelect ; - depositDialog.open({ callback: callback, - next_focus: next_focus, + next_focus: kpsul.article_manager, }); } @@ -505,11 +487,9 @@ $(document).ready(function() { addEdit(amount); } - var next_focus = articleSelect.val() ? articleNb : articleSelect ; - editDialog.open({ callback: callback, - next_focus: next_focus, + next_focus: kpsul.article_manager, }); } @@ -525,11 +505,9 @@ $(document).ready(function() { addWithdraw(amount); } - var next_focus = articleSelect.val() ? articleNb : articleSelect ; - withdrawDialog.open({ callback: callback, - next_focus: next_focus, + next_focus: kpsul.article_manager, }); } @@ -734,7 +712,7 @@ $(document).ready(function() { } else { // F2 - Basket reset resetBasket(); - articleSelect.focus(); + kpsul.article_manager.focus(); } return false; case 114: @@ -766,7 +744,6 @@ $(document).ready(function() { // ----- var env = { - articleSelect: articleSelect, addPurchase: addPurchase, updateBasketAmount: updateBasketAmount, updateBasketRel: updateBasketRel, diff --git a/kfet/urls.py b/kfet/urls.py index dd381a14..9c23b178 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -169,8 +169,6 @@ urlpatterns = [ # ----- url('^k-psul/$', views.kpsul, name='kfet.kpsul'), - url(r'^k-psul/checkout_data/(?P\d+)$', views.kpsul_checkout_data, - name='kfet.kpsul.checkout_data.read'), url('^k-psul/perform_operations$', views.kpsul_perform_operations, name='kfet.kpsul.perform_operations'), url('^k-psul/cancel_operations$', views.kpsul_cancel_operations, diff --git a/kfet/views.py b/kfet/views.py index b7d47022..60ba25ac 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -52,6 +52,32 @@ from .statistic import daynames, monthnames, weeknames, \ this_morning, this_monday_morning, this_first_month_day, \ tot_ventes + +# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/ +class JSONResponseMixin(object): + """ + A mixin that can be used to render a JSON response. + """ + def render_to_json_response(self, context, **response_kwargs): + """ + Returns a JSON response, transforming 'context' to make the payload. + """ + return JsonResponse( + self.get_data(context), + **response_kwargs + ) + + def get_data(self, context): + """ + Returns an object that will be serialized as JSON by json.dumps(). + """ + # Note: This is *EXTREMELY* naive; in reality, you'll need + # to do much more complex handling to ensure that arbitrary + # objects -- such as Django model instances or querysets + # -- can be serialized as JSON. + return context + + class Home(TemplateView): template_name = "kfet/home.html" @@ -618,18 +644,44 @@ class CheckoutCreate(SuccessMessageMixin, CreateView): return super(CheckoutCreate, self).form_valid(form) + # Checkout - Read -class CheckoutRead(DetailView): +class CheckoutRead(JSONResponseMixin, DetailView): model = Checkout template_name = 'kfet/checkout_read.html' context_object_name = 'checkout' def get_context_data(self, **kwargs): - context = super(CheckoutRead, self).get_context_data(**kwargs) - context['statements'] = context['checkout'].statements.order_by('-at') + context = super().get_context_data(**kwargs) + checkout = self.object + if self.request.GET.get('last_statement'): + context['laststatement'] = checkout.statements.latest('at') + else: + context['statements'] = checkout.statements.order_by('-at') return context + def render_to_response(self, context, **kwargs): + if self.request.GET.get('format') == 'json': + data = model_to_dict( + context['checkout'], + fields=['id', 'name', 'balance', 'valid_from', 'valid_to'] + ) + if 'laststatement' in context: + last_statement = context['laststatement'] + last_statement_data = model_to_dict( + last_statement, + fields=['id', 'at', 'balance_new', 'balance_old', 'by'] + ) + last_statement_data['by'] = str(last_statement.by) + # ``at`` is not editable, so skipped by ``model_to_dict`` + last_statement_data['at'] = last_statement.at + data['laststatement'] = last_statement_data + return self.render_to_json_response(data) + else: + return super().render_to_response(context, **kwargs) + + # Checkout - Update class CheckoutUpdate(SuccessMessageMixin, UpdateView): @@ -897,28 +949,6 @@ def kpsul_get_settings(request): return JsonResponse(data) -@teamkfet_required -def kpsul_checkout_data(request, pk): - checkout = get_object_or_404(Checkout, pk=pk) - data = model_to_dict( - checkout, - fields=['id', 'name', 'balance', 'valid_from', 'valid_to'] - ) - - if request.GET.get('last_statement'): - last_statement = checkout.statements.latest('at') - last_statement_data = model_to_dict( - last_statement, - fields=['id', 'at', 'balance_new', 'balance_old', 'by'] - ) - last_statement_data['by'] = str(last_statement.by) - # ``at`` is not editable, so skipped by ``model_to_dict`` - last_statement_data['at'] = last_statement.at - data['laststatement'] = last_statement_data - - return JsonResponse(data) - - @teamkfet_required def kpsul_update_addcost(request): addcost_form = AddcostForm(request.POST) @@ -2145,29 +2175,6 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): # --------------- # Vues génériques # --------------- -# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/ -class JSONResponseMixin(object): - """ - A mixin that can be used to render a JSON response. - """ - def render_to_json_response(self, context, **response_kwargs): - """ - Returns a JSON response, transforming 'context' to make the payload. - """ - return JsonResponse( - self.get_data(context), - **response_kwargs - ) - - def get_data(self, context): - """ - Returns an object that will be serialized as JSON by json.dumps(). - """ - # Note: This is *EXTREMELY* naive; in reality, you'll need - # to do much more complex handling to ensure that arbitrary - # objects -- such as Django model instances or querysets - # -- can be serialized as JSON. - return context class JSONDetailView(JSONResponseMixin,