From eff1b7ff19e04d322419b14180affaf50a08fd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 21 May 2017 20:03:37 +0200 Subject: [PATCH] Cleaning - Article autocomplete, ForestDisplay and more K-Psul - Improve article autocompletion. ArticleManager - "selected" property becomes a reference to an article in the data properties. ForestDisplay - Add data property "object" linked to object being represented - Use class to identify objects instead of id. Allow multiple displays of same ModelForest. - New get_class method returns the class selector to find an object container in the DOM. - New get_dom method returns the DOM element from an object in the ModelForest. Cancellation view - Fix 500 on cancel with already canceled opes/transfers --- kfet/static/kfet/css/kpsul.css | 17 +--- kfet/static/kfet/js/history.js | 30 +++--- kfet/static/kfet/js/kfet.api.js | 42 ++++----- kfet/static/kfet/js/kpsul.js | 162 ++++++++++++++++---------------- kfet/views.py | 2 +- 5 files changed, 121 insertions(+), 132 deletions(-) diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index 32331d07..2631d333 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -254,6 +254,8 @@ input[type=number]::-webkit-outer-spin-button { #article_selection { height:40px; width:100%; + border-bottom: 1px solid #c8102e; + line-height: 39px; } #article_selection input, #article_selection span { @@ -261,7 +263,6 @@ input[type=number]::-webkit-outer-spin-button { float:left; border:0; border-right:1px solid #c8102e; - border-bottom:1px solid #c8102e; border-radius:0; font-size:16px; font-weight:bold; @@ -276,27 +277,17 @@ input[type=number]::-webkit-outer-spin-button { padding-left:10px; } -#article_number { +#article_number, #article_stock { width:10%; text-align:center; } -#article_stock { - width:10%; - line-height:38px; - text-align:center; -} - @media (min-width:1200px) { #article_autocomplete { width:84% } - #article_number { - width:8%; - } - - #article_stock { + #article_number, #article_stock { width:8%; } } diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index ca693e1a..2b634280 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -145,12 +145,14 @@ class KHistory { 'canceled_at': ope.canceled_at, 'canceled_by': ope.canceled_by, }; - if (ope.modelname === 'ope') { - this.data.update('purchase', ope.id, update_data) - || this.data.update('specialope', ope.id, update_data); - } else if (ope.modelname === 'transfer') { - this.data.update('transfer', ope.id, update_data); - } + + let model; + if (ope.modelname === 'ope') + model = Operation; + else if (ope.modelname === 'transfer') + model = Transfer; + + this.data.update(model, ope.id, update_data); } } @@ -165,8 +167,6 @@ class KHistory { } } - var nb_opes = this._$container.find('.ope[canceled="false"]').length; - $('#nb_opes').text(nb_opes); } } @@ -198,14 +198,16 @@ class KHistorySelection { } get_selected() { - var selected = {'transfers': [], 'opes': [],}; + var selected = { + transfers: [], + opes: [] + }; this._$container.find('.ope.ui-selected').each(function() { - var [type, id] = $(this).parent().attr('id').split('-'); - - if (type === 'transfer') - selected['transfers'].push(id); + let object = $(this).parent().data("object"); + if (object instanceof Transfer) + selected.transfers.push(object.id); else - selected['opes'].push(id); + selected.opes.push(object.id); }); return selected; diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 9900fe21..e92d9f54 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -623,7 +623,7 @@ class HistoryGroup extends ModelObject { * @see {@link Models.ModelObject.props|ModelObject.props} */ static get props() { - return ['id', 'at', 'comment', 'valid_by', 'day']; + return ['id', 'at', 'comment', 'valid_by']; } /** @@ -634,8 +634,7 @@ class HistoryGroup extends ModelObject { */ static get default_data() { return { - 'id': 0, 'at': moment(), 'comment': '', - 'valid_by': '', 'day': new Day() + 'id': 0, 'at': null, 'comment': '', 'valid_by': '' }; } @@ -757,7 +756,7 @@ class Operation extends ModelObject { */ static get default_data() { return { - 'id': '', 'amount': 0, 'canceled_at': undefined, 'canceled_by': '', + 'id': '', 'amount': 0, 'canceled_at': null, 'canceled_by': '', }; } @@ -769,7 +768,7 @@ class Operation extends ModelObject { if (v) this._canceled_at = dateUTCToParis(v); else - this._canceled_at = undefined; + this._canceled_at = null; } } @@ -1435,7 +1434,8 @@ class ForestDisplay { options = options || {}; var $container = $('
'); - $container.attr('id', modelname+'-'+node.id); + $container.addClass(this.get_class(node)); + $container.data('object', node); var $rendered = node.display($(template), options); $container.append($rendered); @@ -1459,6 +1459,14 @@ class ForestDisplay { return $container; } + get_class(object) { + return `${object.constructor.verbose_name}-${object.id}`; + } + + get_dom(object) { + return this._$container.find("."+this.get_class(object)); + } + /** * Renders node and adds it to the container.
@@ -1470,15 +1478,15 @@ class ForestDisplay { var existing = this.data.get_parent(node); var first_missing = node; - while (existing && !(this._$container.find('#'+existing.modelname+'-'+existing.id))) { + while (existing && !(this.get_dom(existing))) { first_missing = existing; existing = this.data.get_parent(existing); } var $to_insert = this.render_element(first_missing, options); if (existing) - this._$container - .find('#'+existing.constructor.verbose_name+'-'+existing.id+'>.children') + this.get_dom(existing) + .children(".children") .prepend($to_insert); else this._$container.prepend($to_insert); @@ -1492,12 +1500,9 @@ class ForestDisplay { render(options) { var forest = this.data; - if (forest.is_empty()) - return; - if (forest.constructor.root_sort) forest.roots.sort(forest.constructor.root_sort); - else + else if (forest.roots.length) forest.roots.sort(forest.roots[0].constructor.compare); for (let root of forest.roots) { @@ -1515,13 +1520,12 @@ class ForestDisplay { var modelname = data.constructor.verbose_name; var $new_elt = data.display($(this._templates[modelname]), {}); - var $to_replace = this._$container.find('#'+modelname+'-'+data.id+'>:first-child'); + var $to_replace = this.get_dom(data).children(":first-child"); $to_replace.replaceWith($new_elt); } delete(data) { - let modelname = data.constructor.verbose_name; - this._$container.find('#'+modelname+'-'+data.id).remove(); + this.get_dom(data).remove(); } /** @@ -2055,7 +2059,7 @@ class ItemBasketFormatter extends Formatter { class PurchaseBasketFormatter extends ItemBasketFormatter { static get attrs() { - return ['article_id', 'low_stock']; + return ['low_stock']; } static prop_number(o) { @@ -2066,10 +2070,6 @@ class PurchaseBasketFormatter extends ItemBasketFormatter { return o.article.name; } - static attr_article_id(o) { - return o.article.id; - } - static attr_low_stock(o) { let stock = o.article.stock; return -5 <= stock && stock <= 5; diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index 64508ed6..447dde14 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -32,7 +32,7 @@ class KPsulManager { this.checkout_manager.reset(); this.previous_basket.reset(); Config.reset( () => { - this.article_manager.reset_data(); + this.article_manager.fetch_data(); this.history.fetch(); }); } @@ -282,7 +282,9 @@ class AccountSearch { class CheckoutManager { - constructor() { + constructor(kpsul) { + this.kpsul = kpsul; + this._$container = $('#checkout'); this.display_prefix = '#checkout-'; @@ -314,9 +316,8 @@ class CheckoutManager { this.checkout.get_by_apipk(id, api_options) .done( (data) => this._update_on_success(data) ) - .fail( () => this.reset_data() ); - - kpsul.focus(); + .fail( () => this.reset_data() ) + .always( () => this.kpsul.focus() ); } _update_on_success(data) { @@ -445,7 +446,7 @@ class ArticleManager { this._$nb = $('#article_number'); this._$stock = $('#article_stock'); - this.selected = new Article(); + this.selected = null; this.data = new ArticleList(); var $container = $('#articles_data'); var templates = { @@ -464,7 +465,7 @@ class ArticleManager { } validate(article) { - this.selected.from(article); + this.selected = article; this._$input.val(article.name); this._$nb.val('1'); this._$stock.text('/'+article.stock); @@ -472,22 +473,20 @@ class ArticleManager { } unset() { - this.selected.clear(); + this.selected = null; } is_empty() { - return this.selected.is_empty(); + return !this.selected || this.selected.is_empty(); } - reset_data() { - this.display.clear(); - this.data.clear(); + fetch_data() { this.data.fromAPI(); } update_data(data) { - for (let article_dict of data.articles) - this.data.update('article', article_dict.id, article_dict); + for (let article_data of data.articles) + this.data.update('article', article_data.id, article_data); } reset() { @@ -514,15 +513,13 @@ class ArticleManager { }); this._$container.on('click', '.article', function() { - var id = $(this).parent().attr('id').split('-')[1]; - var article = that.data.find('article', id); - if (article) - that.validate(article); + let article = $(this).parent().data("object"); + that.validate(article); }); this._$nb.on('keydown', function(e) { if (e.keyCode == 13 && that.constructor.check_nb(that.nb) && !that.is_empty()) { - that.kpsul.basket.add_purchase(that.selected.id, parseInt(that.nb)); + that.kpsul.basket.add_purchase(that.selected, parseInt(that.nb)); that.reset().focus(); } @@ -573,85 +570,78 @@ class ArticleAutocomplete { _init_events() { var that = this; - // 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12 - var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/; - var arrowKeys = /^(37|38|39|40)$/; + // 8:Backspace|9:Tab|13:Enter|35:End|36:Home|37-40:Arrows|46:DEL|112-123:F1-F12 + var normalKeys = /^(8|9|13|35|36|37|38|39|40|46|112|113|114|115|116|117|119|120|121|122|123)$/; this._$input .on('keydown', function(e) { - var text = that._$input.val(); - if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.ctrlKey) { - // For the backspace key, we suppose the cursor is at the very end - if(e.keyCode == 8) { - that.update(text.substring(0, text.length-1), true); - } + + if (normalKeys.test(e.keyCode) || e.ctrlKey) return true; - } - that.update(text+e.key, false); + + let initial = that._$input.val(); + let future = initial.substr(0, this.selectionStart) + + e.key + + initial.substr(this.selectionEnd); + + that.update(future); return false; }); + this._$input + .on('input', () => this.update(this._$input.val(), false)); + } - update(prefix, backspace) { + update(prefix, autofill) { + if (autofill === undefined) + autofill = true; - this.resetMatch(); - var article_list = this.manager.data; - var lower = prefix.toLowerCase(); - var that = this; + this.matching = this.find_matching(prefix); - article_list.traverse('article', function(article) { - if (article.name.toLowerCase().startsWith(lower)) - that.matching.push(article); - }); - - if (this.matching.length == 1) { - if (!backspace) { - this.manager.validate(this.matching[0]); - this.showAll(); - } else { - this.manager.unset(); - this.updateDisplay(); - } - } else if (this.matching.length > 1) { + if (this.matching.length === 1 && autofill) { + this.manager.validate(this.matching[0]); + this.showAll(); + } else { this.manager.unset(); - this.updateDisplay(); - if (!backspace) - this.updatePrefix(); + if (this.matching.length >= 1) { + this.updateDisplay(); + if (autofill) + this.updateInput(); + } } } updateDisplay() { - var that = this; + let that = this; + let display = this.manager.display; this.manager.data.traverse('category', function(category) { var is_active = false; for (let article of category.articles) { + let $article = display.get_dom(article); if (that.matching.indexOf(article) != -1) { is_active = true; - that._$container.find('#article-'+article.id).show(); + $article.show(); } else { - that._$container.find('#article-'+article.id).hide(); + $article.hide(); } } - if (is_active) { - that._$container.find('#category-'+category.id).show(); - } else { - that._$container.find('#category-'+category.id).hide(); - } + let $category = display.get_dom(category); + is_active ? $category.show() : $category.hide(); }); } - updatePrefix() { - var lower = this.matching.map(function (article) { - return article.name.toLowerCase(); - }); + updateInput() { + if (!this.matching.length) + return; - lower.sort(); - var first = lower[0], last = lower[lower.length-1], + let names = this.matching.map( article => article.name.toLowerCase() ).sort(); + + let first = names[0], last = names[names.length-1], length = first.length, i = 0; while (i < length && first.charAt(i) === last.charAt(i)) i++; @@ -659,17 +649,20 @@ class ArticleAutocomplete { } showAll() { - var that = this; - this.resetMatch(); - this.manager.data.traverse('article', function(article) { - that.matching.push(article); - }); + this.matching = this.find_matching(""); this.updateDisplay(); } - resetMatch() { - this.matching = []; + find_matching(start) { + let lower = start.toLowerCase(); + let matching = []; + this.manager.data.traverse('article', function(article) { + if (article.name.toLowerCase().startsWith(lower)) + matching.push(article); + }); + return matching; } + } @@ -717,8 +710,8 @@ class BasketManager { return total; } - add_purchase(article_id, nb) { - let found = this.find_purchase(article_id); + add_purchase(article, nb) { + let found = this.find_purchase(article); if (found) { let new_nb = found.article_nb + nb; if (new_nb > 0) { @@ -733,15 +726,15 @@ class BasketManager { } else { let created = this.data.create("purchase", { id: this.formset.new_index(), - article: this.kpsul.article_manager.data.find("article", article_id), + article: article, article_nb: nb }); this.formset.create(created.for_formset()); } } - find_purchase(article_id) { - return this.data.find("purchase", (purchase) => purchase.article.id === article_id); + find_purchase(article) { + return this.data.find("purchase", (purchase) => purchase.article.id === article.id); } add_deposit(amount) { @@ -942,21 +935,24 @@ class BasketSelection { case 46: // DEL (Suppr) basket._$container.find('.ui-selected').each( function() { - let dom_id = $(this).parent().attr("id"); - let id = parseInt(dom_id.split("-")[1]); - basket.delete(id); + let item = $(this).parent().data("object"); + basket.delete(item.id); }); break; case 38: // Arrow up basket._$container.find('.ui-selected').each( function() { - basket.add_purchase(parseInt($(this).attr('article_id')), 1); + let item = $(this).parent().data("object"); + if (item instanceof PurchaseBasket) + basket.add_purchase(item.article, 1); }); break; case 40: // Arrow down basket._$container.find('.ui-selected').each( function() { - basket.add_purchase(parseInt($(this).attr('article_id')), -1); + let item = $(this).parent().data("object"); + if (item instanceof PurchaseBasket) + basket.add_purchase(item.article, -1); }); break; } diff --git a/kfet/views.py b/kfet/views.py index 94f38ec9..1f4495eb 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1286,7 +1286,7 @@ def kpsul_cancel_operations(request): data['errors']['opes_notexisting'] = opes_notexisting return JsonResponse(data, status=400) - already_canceled = {} # Opération/Transfert déjà annulé + already_canceled = defaultdict(list) opes = [] # Pas déjà annulée transfers = [] required_perms = set()