Merge branch 'Aufinal/refactor_articles' into Aufinal/refactor_history

This commit is contained in:
Ludovic Stephan 2017-03-17 00:19:45 -03:00
commit 11603cee69
5 changed files with 219 additions and 199 deletions

View file

@ -296,30 +296,44 @@ input[type=number]::-webkit-outer-spin-button {
#articles_data { #articles_data {
overflow:auto; overflow:auto;
max-height:500px; max-height:500px;
}
#articles_data table {
width: 100%; width: 100%;
} }
#articles_data table tr.article { #articles_data div.article {
height:25px; height:25px;
font-size:14px; font-size:14px;
} }
#articles_data table tr.article>td:first-child { #articles_data span {
padding-left:10px; height:25px;
line-height:25px;
display: inline-block;
} }
#articles_data table tr.category { #articles_data span.name {
padding-left:10px;
width:78%;
}
#articles_data span.price {
width:8%;
}
#articles_data span.stock {
width:14%;
}
#articles_data div.category {
height:35px; height:35px;
line-height:35px;
background-color:#c8102e; background-color:#c8102e;
font-size:16px; font-size:16px;
color:#FFF; color:#FFF;
font-weight:bold; font-weight:bold;
} }
#articles_data table tr.category>td:first-child { #articles_data div.category>span:first-child {
padding-left:20px; padding-left:20px;
} }

View file

@ -71,8 +71,10 @@ class Config {
* A model subclasses {@link Models.ModelObject}.<br> * A model subclasses {@link Models.ModelObject}.<br>
* A model whose instances can be got from API subclasses * A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.<br> * {@link Models.APIModelObject}.<br>
* A model to manage ModelObject lists * A model to manage ModelObject forests
* {@link Models.ModelList}.<br> * {@link Models.ModelForest}.<br>
* A ModelObject that can be fetched through API
* {@link Models.APIModelForest}.<br>
* These classes should not be used directly. * These classes should not be used directly.
* <br><br> * <br><br>
* *
@ -81,7 +83,9 @@ class Config {
* {@link Models.Checkout} (partial). * {@link Models.Checkout} (partial).
* <br> * <br>
* Models without API support: * Models without API support:
* {@link Models.Statement}. * {@link Models.Statement},
* {@link Models.ArticleCategory},
* {@link Models.Article}.
* *
* @namespace Models * @namespace Models
*/ */
@ -107,13 +111,6 @@ class ModelObject {
*/ */
static get default_data() { return {}; } static get default_data() { return {}; }
/**
* Verbose name so refer to this model
* @abstract
* @type {string}
*/
static get verbose_name() { return ""; }
/** /**
* Create new instance from data or default values. * Create new instance from data or default values.
* @param {Object} [data={}] - data to store in instance * @param {Object} [data={}] - data to store in instance
@ -171,15 +168,12 @@ class ModelObject {
} }
/** /**
* Compare function between two instances of the model * Returns a string value for the model, to use in comparisons
* @abstract * @see {@link Models.TreeNode.compare|TreeNode.compare}
* @param {a} Models.ModelObject
* @param {b} Models.ModelObject
*/ */
static compare(a, b) { comparevalue() {
return a.id - b.id ; return this.id.toString();
} }
} }
@ -493,13 +487,6 @@ class ArticleCategory extends ModelObject {
return {'id': 0, 'name': ''}; return {'id': 0, 'name': ''};
} }
/**
* Verbose name for ArticleCategory model.
* @default <tt>'article_category'</tt>
* @see {@link Models.ModelObject.verbose_name[ModelObject.verbose_name}
*/
static get verbose_name() { return 'category'; }
/** /**
* @default {@link Formatters.ArticleCategoryFormatter} * @default {@link Formatters.ArticleCategoryFormatter}
*/ */
@ -511,8 +498,8 @@ class ArticleCategory extends ModelObject {
* Comparison function between ArticleCategory model instances. * Comparison function between ArticleCategory model instances.
* @see {@link Models.ModelObject.compare|ModelObject.compare} * @see {@link Models.ModelObject.compare|ModelObject.compare}
*/ */
static compare(a, b) { comparevalue() {
return a.name.localeCompare(b.name); return this.name ;
} }
} }
@ -544,13 +531,6 @@ class Article extends ModelObject {
}; };
} }
/**
* Verbose name for Article model
* @default <tt>'article'</tt>
* @see {@link Models.ModelObject.verbose_name|ModelObject.verbose_name}
*/
static get verbose_name() { return 'article'; }
/** /**
* @default {@link Formatters.ArticleFormatter} * @default {@link Formatters.ArticleFormatter}
*/ */
@ -562,8 +542,8 @@ class Article extends ModelObject {
* Comparison function between Article model instances. * Comparison function between Article model instances.
* @see {@link Models.ModelObject.compare|ModelObject.compare} * @see {@link Models.ModelObject.compare|ModelObject.compare}
*/ */
static compare(a, b) { comparevalue() {
return a.name.localeCompare(b.name); return this.name;
} }
// Take care of 'price' type // Take care of 'price' type
@ -757,136 +737,148 @@ class Operation extends ModelObject {
/** /**
* Simple {@link Models.ModelObject} list. * Node for ModelForest object
* @memberof Models * @memberof Models
*/ */
class ModelList { class TreeNode {
/** constructor(type, content) {
* Nested structure of the list this.type = type;
* @abstract this.content = content;
* @type {Models.ModelObject[]} this.parent = null;
*/ this.children = [];
static get models() { return []; }
/**
* Verbose names for list models
* @abstract
* @type {string[]}
*/
static get names() {
return this.models.map(function(v) {
return v.verbose_name;
});
} }
static compare(a, b) {
var a_serial = a.content.comparevalue();
var b_serial = b.content.comparevalue();
return a_serial.localeCompare(b_serial)
}
}
/**
* Simple {@link Models.ModelObject} forest.
* @memberof Models
*/
class ModelForest {
/**
* Dictionary associating types to classes
* @abstract
* @type {Object}
*/
static get models() { return {}; }
/** /**
* Creates empty instance and populates it with data if given * Creates empty instance and populates it with data if given
* @param {Object[]} [datalist=[]] * @param {Object[]} [datalist=[]]
*/ */
constructor(datalist) { constructor(datalist) {
this.data = {};
this.from(datalist || []); this.from(datalist || []);
} }
/** /**
* Fetches an object from the instance data, or creates it if * Fetches an object from the instance data, or creates it if
* it does not exist yet.<br> * it does not exist yet.<br>
* Parent objects are created recursively if needed. * If direction >= 0, parent objects are created recursively.
* @param {number} depth depth on the nested structure of the list * If direction <= 0, child objects are created recursively.
* @param {Object} data * @param {Object} data
* @param {number} direction
*/ */
get_or_create(depth, data) { get_or_create(data, direction) {
var model = this.constructor.models[depth]; var model = this.constructor.models[data.type];
var name = model.verbose_name ;
var existing = this.data[name].find(function (v){
return v.id === data['id'] ;
}) ;
var existing = this.find(data.type, data.content.id);
if (existing) { if (existing) {
return existing; return existing;
} }
if (depth == this.constructor.models.length-1) { var content = new this.constructor.models[data.type](data.content);
var created = new model(data) ; var node = new TreeNode(data.type, content);
this.data[name].push(created);
return created ;
} else {
var par_name = this.constructor.models[depth+1]
.verbose_name;
var par_data = data[par_name]; if (direction <= 0) {
var created = new model(data); if (data.parent) {
var parnt = this.get_or_create(depth+1, par_data); var parent = this.get_or_create(data.parent, -1);
created[par_name] = parnt; node.parent = parent;
parent.children.push(node);
this.data[name].push(created); } else {
return created ; this.roots.push(node);
}
} }
if (direction >= 0 && data.children) {
for (let child_data of data.children) {
var child = this.get_or_create(child_data, 1);
child.parent = node;
node.children.push(child);
}
}
return node ;
} }
/** /**
* Resets then populates the instance with the given data, starting from * Resets then populates the instance with the given data.
* the lowest level Models.ModelObject in {@link Models.ModelList#models|models}.<br>
* @param {Object[]} datalist * @param {Object[]} datalist
*/ */
from(datalist) { from(datalist) {
this.roots = [];
for (let key of this.constructor.names) {
this.data[key] = [];
}
for (let data of datalist) { for (let data of datalist) {
this.get_or_create(0, data); this.get_or_create(data, 0);
} }
} }
/** /**
* Removes all Models.ModelObject from the list. * Removes all Models.TreeNode from the tree.
*/ */
clear() { clear() {
this.from([]); this.from([]);
} }
/** /**
* Renders an element (and all its offspring) and returns the * Renders a node (and all its offspring) and returns the
* corresponding jQuery object. * corresponding jQuery object.
* @param {Models.ModelObject} elt * @param {Models.TreeNode} node
* @param {Object} templates Templates to render each model * @param {Object} templates Templates to render each model
* @param {Object} [options] Options for element render method * @param {Object} [options] Options for element render method
*/ */
render_element(elt, templates, options) { render_element(node, templates, options) {
var name = elt.constructor.verbose_name; var template = templates[node.type];
var depth = this.constructor.names.indexOf(name);
// Allows for more granular template specification
var template = templates[elt.type] || templates[name];
var options = options || {} ; var options = options || {} ;
if (depth == -1) { var $container = $('<div></div>');
return $(); $container.attr('id', node.type+'-'+node.content.id);
} else if (depth == 0) {
var $rendered = elt.display($(template), options);
$rendered.attr('data-'+name+'-id', elt.id);
return $rendered;
} else {
var child_model = this.constructor.models[depth-1];
var children = this.data[child_model.verbose_name]
.filter(v => v[name].id == elt.id) ;
children.sort(child_model.compare);
//TODO: less dirty var $rendered = node.content.display($(template), options);
var $container = $('<div></div>'); $container.append($rendered);
var $elt = elt.display($(template), options);
$elt.attr('data-'+name+'-id', elt.id);
$container.append($elt);
for (let child of children) { //TODO: better sorting control
$container.append(this.render_element(child, templates, options)); node.children.sort(TreeNode.compare);
}
return $container.html(); for (let child of node.children) {
var $child = this.render_element(child, templates, options);
$container.append($child);
} }
return $container;
}
add_to_container($container, node, templates, options) {
var existing = node.parent ;
var first_missing = node;
while (existing && !($container.find('#'+existing.type+'-'+existing.id))) {
first_missing = existing ;
existing = existing.parent;
}
var $to_insert = render_element(first_missing, templates, options);
var $insert_in = existing ? $container.find('#'+existing.type+'-'+existing.id)
: $container ;
$insert_in.prepend($to_insert);
} }
/** /**
@ -896,44 +888,53 @@ class ModelList {
* @param {Object} [options] Options for element render method * @param {Object} [options] Options for element render method
*/ */
display($container, templates, options) { display($container, templates, options) {
var root_model = this.constructor.models[this.constructor.models.length-1]; this.roots.sort(TreeNode.compare);
var roots = this.data[root_model.verbose_name];
roots.sort(root_model.compare);
for (let root of roots) { for (let root of this.roots) {
$container.append(this.render_element(root, templates, options)); $container.append(this.render_element(root, templates, options));
} }
return $container; return $container;
} }
traverse(callback) {
function recurse(node) {
callback(node) ;
for (let child of node.children)
callback(child);
}
for (let root of this.roots)
recurse(root);
}
/** /**
* Find instance in data matching given properties. * Find instance in data matching given properties.
* @param {class} model * @param {class} model
* @param {Object} props Properties to match * @param {Object} props Properties to match
*/ */
find(model, props) { find(type, id) {
if (this.constructor.models.indexOf(model) == -1) { var result = null;
return undefined ; function callback(node) {
if (node.type === type && node.content.id == id)
result = node ;
} }
return this.data[model.verbose_name].find(function(v) { this.traverse(callback);
for (let key in props) {
if (v[key] !== props[key]) return result ;
return false;
}
return true;
});
} }
} }
/** /**
* Describes a model list that can be filled through API. * Describes a model list that can be filled through API.
* @extends Models.ModelList * @extends Models.ModelForest
* @memberof Models * @memberof Models
*/ */
class APIModelList extends ModelList { class APIModelForest extends ModelForest {
/** /**
* Request url to fill the model. * Request url to fill the model.
@ -944,7 +945,7 @@ class APIModelList extends ModelList {
/** /**
* Fills the instance with distant data. It sends a GET HTTP request to * Fills the instance with distant data. It sends a GET HTTP request to
* {@link Models.APIModelList#url_model}. * {@link Models.APIModelForest#url_model}.
* @param {object} [api_options] Additional data appended to the request. * @param {object} [api_options] Additional data appended to the request.
* @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds. * @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. * @param {jQueryAjaxError} [on_error] A function to be called if the request fails.
@ -970,24 +971,26 @@ class APIModelList extends ModelList {
/** /**
* ArticleList model. Can be accessed through API. * ArticleList model. Can be accessed through API.
* @extends Models.APIModelList * @extends Models.APIModelForest
* @memberof Models * @memberof Models
*/ */
class ArticleList extends APIModelList { class ArticleList extends APIModelForest {
/** /**
* Default structure for ArticleList instances * Default structure for ArticleList instances
* @default <tt>[Article, ArticleCategory]</tt> * @abstract
* @see {@link Models.ModelList.models|ModelList.models} * @default <tt>{'article': Article,
'category': ArticleCategory}</tt>
*/ */
static get models() { static get models() {
return [Article, ArticleCategory]; return {'article': Article,
'category': ArticleCategory};
} }
/** /**
* Default url to get ArticleList data * Default url to get ArticleList data
* @default <tt>django-js-reverse('kfet.kpsul.articles_data')</tt> * @default <tt>django-js-reverse('kfet.kpsul.articles_data')</tt>
* @see {@link Models.APIModelList.url_model|APIModelList.url_model} * @see {@link Models.APIModelForest.url_model|APIModelForest.url_model}
*/ */
static get url_model() { static get url_model() {
return Urls['kfet.kpsul.articles_data'](); return Urls['kfet.kpsul.articles_data']();

View file

@ -373,11 +373,11 @@ class ArticleManager {
constructor(env) { constructor(env) {
this._env = env; // Global K-Psul Manager this._env = env; // Global K-Psul Manager
this._$container = $('#articles_data tbody'); this._$container = $('#articles_data');
this._$input = $('#article_autocomplete'); this._$input = $('#article_autocomplete');
this._$nb = $('#article_number'); this._$nb = $('#article_number');
this.templates = {'category': '<tr class="category"><td class="name" colspan="3"></td></tr>', this.templates = {'category': '<div class="category"><span class="name"></span></div>',
'article' : '<tr class="article"><td class="name"></td><td class="price"></td><td class="stock"></td></tr>'} 'article' : '<div class="article"><span class="name"></span><span class="price"></span><span class="stock"></span></div>'}
this.selected = new Article() ; this.selected = new Article() ;
this.list = new ArticleList() ; this.list = new ArticleList() ;
@ -410,19 +410,19 @@ class ArticleManager {
} }
reset_data() { reset_data() {
this._$container.find('tr').remove(); this._$container.html('');
this.list.clear(); this.list.clear();
this.list.fromAPI({}, this.display_list.bind(this), $.noop) ; this.list.fromAPI({}, this.display_list.bind(this), $.noop) ;
} }
update_data(data) { update_data(data) {
for (let article_dict of data) { for (let article_dict of data) {
article = this.list.find(Article, {'id': article_dict['id']}); article = this.list.find('article', article_dict['id']);
// For now, article additions are disregarded // For now, article additions are disregarded
if (article) { if (article) {
article.stock = article_dict['stock']; article.stock = article_dict['stock'];
this._$container.find('#data-article-'+article_dict['id']+' .stock') this._$container.find('#article-'+article_dict['id']+' .stock')
.text(article_dict['stock']); .text(article_dict['stock']);
} }
} }
@ -450,10 +450,11 @@ class ArticleManager {
}); });
this._$container.on('click', '.article', function() { this._$container.on('click', '.article', function() {
var id = $(this).attr('data-article-id') ; var id = $(this).parent().attr('id').split('-')[1];
var article = that.list.find(Article, { 'id': intCheck(id) }); console.log(id);
var article = that.list.find('article', id);
if (article) if (article)
that.validate(article); that.validate(article.content);
}); });
this._$nb.on('keydown', function(e) { this._$nb.on('keydown', function(e) {
@ -488,8 +489,6 @@ class ArticleAutocomplete {
constructor(article_manager) { constructor(article_manager) {
this.manager = article_manager; this.manager = article_manager;
this.matching = [];
this.active_categories = [];
this._$container = article_manager._$container ; this._$container = article_manager._$container ;
this._$input = $('#article_autocomplete'); this._$input = $('#article_autocomplete');
@ -523,31 +522,30 @@ class ArticleAutocomplete {
update(prefix, backspace) { update(prefix, backspace) {
this.resetMatch();
var article_list = this.manager.list ; var article_list = this.manager.list ;
var lower = prefix.toLowerCase() ; var lower = prefix.toLowerCase() ;
var that = this ; var that = this ;
this.matching = article_list.data['article']
.filter(function(article) { article_list.traverse(function(node) {
return article.name.toLowerCase() if (node.type === 'article' &&
.indexOf(lower) === 0 ; node.content.name.toLowerCase()
.startsWith(lower)) {
that.matching['article'].push(node.content);
if (that.matching['category'].indexOf(node.parent.content) == -1)
that.matching['category'].push(node.parent.content);
}
}); });
this.active_categories = article_list.data['category'] if (this.matching['article'].length == 1) {
.filter(function(category) {
return that.matching.find(function(article) {
return article.category === category ;
});
});
if (this.matching.length == 1) {
if (!backspace) { if (!backspace) {
this.manager.validate(this.matching[0]) ; this.manager.validate(this.matching['article'][0]) ;
this.showAll() ; this.showAll() ;
} else { } else {
this.manager.unset(); this.manager.unset();
this.updateDisplay(); this.updateDisplay();
} }
} else if (this.matching.length > 1) { } else if (this.matching['article'].length > 1) {
this.manager.unset(); this.manager.unset();
this.updateDisplay() ; this.updateDisplay() ;
if (!backspace) if (!backspace)
@ -556,26 +554,21 @@ class ArticleAutocomplete {
} }
updateDisplay() { updateDisplay() {
var article_list = this.manager.list ; var that = this;
for (let article of article_list.data['article']) {
if (this.matching.indexOf(article) > -1) {
this._$container.find('[data-article-id='+article.id+']').show();
} else {
this._$container.find('[data-article-id='+article.id+']').hide();
}
}
for (let category of article_list.data['category']) { this.manager.list.traverse(function(node) {
if (this.active_categories.indexOf(category) > -1) { if (that.matching[node.type].indexOf(node.content) != -1) {
this._$container.find('[data-category-id='+category.id+']').show(); that._$container.find('#'+node.type+'-'+node.content.id)
} else { .show();
this._$container.find('[data-category-id='+category.id+']').hide(); } else {
that._$container.find('#'+node.type+'-'+node.content.id)
.hide();
} }
} });
} }
updatePrefix() { updatePrefix() {
var lower = this.matching.map(function (article) { var lower = this.matching['article'].map(function (article) {
return article.name.toLowerCase() ; return article.name.toLowerCase() ;
}); });
@ -588,8 +581,16 @@ class ArticleAutocomplete {
} }
showAll() { showAll() {
this.matching = this.manager.list.data['article']; var that = this;
this.active_categories = this.manager.list.data['category']; this.resetMatch();
this.manager.list.traverse(function(node) {
that.matching[node.type].push(node.content);
});
this.updateDisplay(); this.updateDisplay();
} }
resetMatch() {
this.matching = {'article' : [],
'category': []};
}
} }

View file

@ -131,10 +131,6 @@
<input type="hidden" id="article_id" value=""> <input type="hidden" id="article_id" value="">
</div> </div>
<div id="articles_data"> <div id="articles_data">
<table>
<tbody>
</tbody>
</table>
</div> </div>
</div> </div>
<div class="row kpsul_middle_left_bottom"> <div class="row kpsul_middle_left_bottom">
@ -833,10 +829,10 @@ $(document).ready(function() {
function hardReset(give_tri_focus=true) { function hardReset(give_tri_focus=true) {
coolReset(give_tri_focus); coolReset(give_tri_focus);
kpsul.checkout_manager.reset(); kpsul.checkout_manager.reset();
kpsul.article_manager.reset_data();
resetPreviousOp(); resetPreviousOp();
khistory.reset(); khistory.reset();
Config.reset(function() { Config.reset(function() {
kpsul.article_manager.reset_data();
displayAddcost(); displayAddcost();
getHistory(); getHistory();
}); });

View file

@ -1479,13 +1479,19 @@ def kpsul_articles_data(request):
for article in articles: for article in articles:
articlelist.append({ articlelist.append({
'id': article.id, 'type': 'article',
'name': article.name, 'content': {
'price': article.price, 'id': article.id,
'stock': article.stock, 'name': article.name,
'category': { 'price': article.price,
'id': article.category.id, 'stock': article.stock,
'name': article.category.name, },
'parent': {
'type': 'category',
'content': {
'id': article.category.id,
'name': article.category.name,
},
} }
}) })
return JsonResponse(articlelist, safe=False) return JsonResponse(articlelist, safe=False)