add Article and Category models
This commit is contained in:
parent
a9cb50b38d
commit
643503269e
2 changed files with 500 additions and 0 deletions
|
@ -449,6 +449,79 @@ class Statement extends ModelObject {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ArticleCategory model. Cannot be accessed through API.
|
||||||
|
* @extends Models.ModelObject
|
||||||
|
* @memberof Models
|
||||||
|
*/
|
||||||
|
class ArticleCategory extends ModelObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties associated to a category
|
||||||
|
* @default <tt>['id', 'name']</tt>
|
||||||
|
* @see {@link Models.ModelObject.props|ModelObject.props}
|
||||||
|
*/
|
||||||
|
static get props() {
|
||||||
|
return ['id', 'name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default values for ArticleCategory model instances.
|
||||||
|
* @default <tt>{ 'id': 0, 'name': '' }</tt>
|
||||||
|
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
|
||||||
|
*/
|
||||||
|
static get default_data() {
|
||||||
|
return {'id': 0, 'name': ''};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @default {@link Formatters.ArticleCategoryFormatter}
|
||||||
|
*/
|
||||||
|
formatter() {
|
||||||
|
return ArticleCategoryFormatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Article model. Cannot be accessed through API.
|
||||||
|
* @extends Models.ModelObject
|
||||||
|
* @memberof Models
|
||||||
|
*/
|
||||||
|
class Article extends ModelObject {
|
||||||
|
/**
|
||||||
|
* Properties associated to an article
|
||||||
|
* @default <tt>['id', 'name']</tt>
|
||||||
|
* @see {@link Models.ModelObject.props|ModelObject.props}
|
||||||
|
*/
|
||||||
|
static get props() {
|
||||||
|
return ['id', 'name', 'price', 'stock', 'category'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default values for Article model instances.
|
||||||
|
* @default <tt>{ 'id': 0, 'name': '', 'price': 0, 'stock': 0,
|
||||||
|
* 'category': new ArticleCategory() }</tt>
|
||||||
|
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
|
||||||
|
*/
|
||||||
|
static get default_data() {
|
||||||
|
return {
|
||||||
|
'id': 0, 'name': '', 'price': 0, 'stock': 0,
|
||||||
|
'category': new ArticleCategory(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @default {@link Formatters.ArticleCategoryFormatter}
|
||||||
|
*/
|
||||||
|
formatter() {
|
||||||
|
return ArticleFormatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take care of 'price' type
|
||||||
|
// API currently returns a string object (serialization of Decimal type within Django)
|
||||||
|
get price() { return this._price; }
|
||||||
|
set price(v) { this._price = floatCheck(v); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- ---------- */
|
/* ---------- ---------- */
|
||||||
|
|
||||||
|
@ -683,3 +756,33 @@ class StatementFormatter extends Formatter {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @memberof Formatters
|
||||||
|
* @extends Formatters.Formatter
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ArticleCategoryFormatter extends Formatter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties renderable to html.
|
||||||
|
* @default {@link Models.Statement.props}
|
||||||
|
*/
|
||||||
|
static get props() {
|
||||||
|
return ArticleCategory.props;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get _data_stock() {
|
||||||
|
return {
|
||||||
|
'default': '', 'low': 'low',
|
||||||
|
'ok': 'ok', 'neg': 'neg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static attr_data_stock(a) {
|
||||||
|
if (a.stock >= 5) { return this._data_stock.ok; }
|
||||||
|
else if (a.stock >= -5) { return this._data_stock.low; }
|
||||||
|
else /* a.stock < -5 */ { return this._data_stock.neg; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -189,6 +189,403 @@ function booleanCheck(v) {
|
||||||
return v == true;
|
return v == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function functionCheck(v) {
|
||||||
|
if (typeof v === 'function')
|
||||||
|
return v;
|
||||||
|
return function(){};
|
||||||
|
}
|
||||||
|
|
||||||
|
function restrict(obj, attr_list) {
|
||||||
|
var restricted = {} ;
|
||||||
|
for (let attr of attr_list) {
|
||||||
|
restricted[attr] = obj[attr] ;
|
||||||
|
}
|
||||||
|
return restricted ;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleCategory {
|
||||||
|
|
||||||
|
constructor(id, name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() { return this._id; }
|
||||||
|
|
||||||
|
set id(v) { this._id = intCheck(v); }
|
||||||
|
|
||||||
|
html(template) {
|
||||||
|
var $template = $(template)
|
||||||
|
$template.attr('id', 'data-category-'+this.id);
|
||||||
|
$template.find('td').text(this.name);
|
||||||
|
return $template ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compare(a, b) {
|
||||||
|
return a.name.localeCompare(b.name) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Article {
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
$.extend(this, this.constructor.default_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get default_data() {
|
||||||
|
return {
|
||||||
|
'id': 0, 'name': '', 'price': 0, 'stock': 0,
|
||||||
|
'category': new ArticleCategory(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() { return this._id; }
|
||||||
|
get price() { return this._price; }
|
||||||
|
get stock() { return this._stock; }
|
||||||
|
|
||||||
|
set id(v) { this._id = intCheck(v); }
|
||||||
|
set price(v) { this._price = floatCheck(v); }
|
||||||
|
set stock(v) { this._stock = intCheck(v); }
|
||||||
|
|
||||||
|
get str_price_ukf() {
|
||||||
|
return amountToUKF(this.price, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get _data_stock_ok() { return ''; }
|
||||||
|
static get _data_stock_low() { return 'low'; }
|
||||||
|
static get _data_stock_neg() { return 'neg'; }
|
||||||
|
|
||||||
|
get data_stock() {
|
||||||
|
var stock = this.stock ;
|
||||||
|
if (stock >= 5) { return this.constructor._data_stock_ok; }
|
||||||
|
else if (stock >= -5) { return this.constructor._data_stock_low; }
|
||||||
|
else /* stock < -5 */ { return this.constructor._data_stock_neg; }
|
||||||
|
}
|
||||||
|
|
||||||
|
from(data) {
|
||||||
|
$.extend(this, this.constructor.default_data, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
html(template) {
|
||||||
|
var $template = $(template);
|
||||||
|
$template
|
||||||
|
.find('.name').text(this.name).end()
|
||||||
|
.find('.price').text(this.str_price_ukf).end()
|
||||||
|
.find('.stock').text(this.stock).end();
|
||||||
|
$template.attr('id', 'data-article-'+this.id);
|
||||||
|
$template.attr('data-stock', this.data_stock);
|
||||||
|
return $template ;
|
||||||
|
}
|
||||||
|
|
||||||
|
static compare(a, b) {
|
||||||
|
return a.name.localeCompare(b.name) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.from({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleList {
|
||||||
|
constructor() {
|
||||||
|
this.articles = [];
|
||||||
|
this.categories = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get_or_create(id, name) {
|
||||||
|
var category = this.categories.find(c => c.id === id);
|
||||||
|
if (!category) {
|
||||||
|
category = new ArticleCategory(id, name);
|
||||||
|
this.categories.push(category);
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
from(data, callback) {
|
||||||
|
callback = functionCheck(callback);
|
||||||
|
|
||||||
|
for (let article_data of data['articles']) {
|
||||||
|
var category = this.get_or_create(article_data['category_id'], article_data['category__name']) ;
|
||||||
|
article_data = restrict(article_data, ['name', 'price', 'stock', 'id']) ;
|
||||||
|
article_data['category'] = category ;
|
||||||
|
|
||||||
|
var article = new Article() ;
|
||||||
|
article.from(article_data) ;
|
||||||
|
this.articles.push(article) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAPI(on_success, on_error) {
|
||||||
|
on_error = functionCheck(on_error);
|
||||||
|
var that = this ;
|
||||||
|
$.ajax({
|
||||||
|
dataType: "json",
|
||||||
|
url : "{% url 'kfet.kpsul.articles_data' %}",
|
||||||
|
method : "GET",
|
||||||
|
})
|
||||||
|
.done((data) => this.from(data, on_success))
|
||||||
|
.fail(on_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
display($container, $article_template, $category_template) {
|
||||||
|
this.categories.sort(ArticleCategory.compare) ;
|
||||||
|
for (let cat of this.categories) {
|
||||||
|
var cat_articles = this.articles.filter(a => a.category.id === cat.id) ;
|
||||||
|
cat_articles.sort(Article.compare);
|
||||||
|
|
||||||
|
$container.append(cat.html($category_template)) ;
|
||||||
|
for (let art of cat_articles) {
|
||||||
|
$container.append(art.html($article_template)) ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.articles = [];
|
||||||
|
this.categories = [] ;
|
||||||
|
}
|
||||||
|
|
||||||
|
find_by_id(name) {
|
||||||
|
return this.articles.find(function(a) {
|
||||||
|
return a.name === name ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get_from_elt($elt) {
|
||||||
|
var id = $elt.attr('id').split('-')[2];
|
||||||
|
|
||||||
|
return this.articles.find(function(article) {
|
||||||
|
return (article.id == id) ;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleManager {
|
||||||
|
|
||||||
|
constructor(env) {
|
||||||
|
this._env = env; // Global K-Psul Manager
|
||||||
|
|
||||||
|
this._$container = $('#articles_data tbody');
|
||||||
|
this._$input = $('#article_autocomplete');
|
||||||
|
this._$nb = $('#article_number');
|
||||||
|
this._category_template = '<tr class="category"><td colspan="3"></td></tr>';
|
||||||
|
this._article_template = '<tr class="article"><td class="name"></td><td class="price"></td><td class="stock"></td></tr>';
|
||||||
|
|
||||||
|
this.selected = new Article() ;
|
||||||
|
this.list = new ArticleList() ;
|
||||||
|
this.autocomplete = new ArticleAutocomplete(this);
|
||||||
|
|
||||||
|
this._init_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
get nb() {
|
||||||
|
return this._$nb.val() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
display_list() {
|
||||||
|
this.list.display(this._$container, this._article_template, this._category_template) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(article) {
|
||||||
|
this.selected.from(article) ;
|
||||||
|
this._$input.val(article.name);
|
||||||
|
this._$nb.val('1');
|
||||||
|
this._$nb.focus().select();
|
||||||
|
}
|
||||||
|
|
||||||
|
unset() {
|
||||||
|
this.selected.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
is_empty() {
|
||||||
|
return this.selected.id == 0 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_data() {
|
||||||
|
this._$container.find('tr').remove();
|
||||||
|
this.list.clear();
|
||||||
|
this.list.fromAPI(this.display_list.bind(this)) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_data(data) {
|
||||||
|
for (let article_dict of data) {
|
||||||
|
var article = this.list.articles.find(function(art) {
|
||||||
|
return (art.id == article_dict['id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// For now, article additions are disregarded
|
||||||
|
if (article) {
|
||||||
|
article.stock = article_dict['stock'];
|
||||||
|
this._$container.find('#data-article-'+article_dict['id']+' .stock')
|
||||||
|
.text(article_dict['stock']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.unset() ;
|
||||||
|
this._$nb.val('');
|
||||||
|
this._$input.val('');
|
||||||
|
this.autocomplete.showAll() ;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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)$/;
|
||||||
|
|
||||||
|
//Global input event (to merge ?)
|
||||||
|
this._$input.on('keydown', function(e) {
|
||||||
|
if (e.keyCode == 13 && that._$input.val() == '') {
|
||||||
|
that._env.performOperations();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._$container.on('click', '.article', function() {
|
||||||
|
var article = that.list.get_from_elt($(this)) ;
|
||||||
|
that.validate(article);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._$nb.on('keydown', function(e) {
|
||||||
|
if (e.keyCode == 13 && ArticleManager.check_nb(that.nb) && !that.is_empty()) {
|
||||||
|
that._env.addPurchase(that.selected, that.nb);
|
||||||
|
that.reset();
|
||||||
|
that.focus();
|
||||||
|
}
|
||||||
|
if (normalKeys.test(e.keyCode) || arrowKeys.test(e.KeyCode) || e.ctrlKey) {
|
||||||
|
if (e.ctrlKey && e.charCode == 65)
|
||||||
|
that._$nb.val('');
|
||||||
|
return true ;
|
||||||
|
}
|
||||||
|
if (ArticleManager.check_nb(that.nb+e.key))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this._$input.focus();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static check_nb(nb) {
|
||||||
|
return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24 ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleAutocomplete {
|
||||||
|
|
||||||
|
constructor(article_manager) {
|
||||||
|
this.manager = article_manager;
|
||||||
|
this.matching = [];
|
||||||
|
this.active_categories = [];
|
||||||
|
this._$container = article_manager._$container ;
|
||||||
|
this._$input = $('#article_autocomplete');
|
||||||
|
|
||||||
|
this.showAll() ;
|
||||||
|
this._init_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
_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)$/;
|
||||||
|
|
||||||
|
this._$input
|
||||||
|
.on('keydown', function(e) {
|
||||||
|
var text = that._$input.val() ;
|
||||||
|
if (normalKeys.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);
|
||||||
|
}
|
||||||
|
return true ;
|
||||||
|
}
|
||||||
|
that.update(text+e.key, false);
|
||||||
|
|
||||||
|
return false ;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
update(prefix, backspace) {
|
||||||
|
|
||||||
|
var article_list = this.manager.list ;
|
||||||
|
var lower = prefix.toLowerCase() ;
|
||||||
|
var that = this ;
|
||||||
|
this.matching = article_list.articles.filter(function(article) {
|
||||||
|
return article.name.toLowerCase()
|
||||||
|
.indexOf(lower) === 0 ;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.active_categories = article_list.categories.filter(function(category) {
|
||||||
|
return that.matching.find(function(article) {
|
||||||
|
return article.category === category ;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.manager.unset();
|
||||||
|
this.updateDisplay() ;
|
||||||
|
if (!backspace)
|
||||||
|
this.updatePrefix();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplay() {
|
||||||
|
var article_list = this.manager.list ;
|
||||||
|
for (let article of article_list.articles) {
|
||||||
|
if (this.matching.indexOf(article) > -1) {
|
||||||
|
this._$container.find('#data-article-'+article.id).show();
|
||||||
|
} else {
|
||||||
|
this._$container.find('#data-article-'+article.id).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let category of article_list.categories) {
|
||||||
|
if (this.active_categories.indexOf(category) > -1) {
|
||||||
|
this._$container.find('#data-category-'+category.id).show();
|
||||||
|
} else {
|
||||||
|
this._$container.find('#data-category-'+category.id).hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePrefix() {
|
||||||
|
var lower = this.matching.map(function (article) {
|
||||||
|
return article.name.toLowerCase() ;
|
||||||
|
});
|
||||||
|
|
||||||
|
lower.sort() ;
|
||||||
|
var first = lower[0], last = lower[lower.length-1],
|
||||||
|
length = first.length, i = 0;
|
||||||
|
while (i < length && first.charAt(i) === last.charAt(i)) i++;
|
||||||
|
|
||||||
|
this._$input.val(first.substring(0,i)) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAll() {
|
||||||
|
this.matching = this.manager.list.articles;
|
||||||
|
this.active_categories = this.manager.list.categories;
|
||||||
|
this.updateDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
Loading…
Reference in a new issue