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
This commit is contained in:
Aurélien Delobelle 2017-05-21 20:03:37 +02:00
parent 51083f9195
commit eff1b7ff19
5 changed files with 121 additions and 132 deletions

View file

@ -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%;
}
}

View file

@ -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;

View file

@ -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 = $('<div></div>');
$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.<br>
@ -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;

View file

@ -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;
}

View file

@ -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()