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 {
overflow:auto;
max-height:500px;
}
#articles_data table {
width: 100%;
}
#articles_data table tr.article {
#articles_data div.article {
height:25px;
font-size:14px;
}
#articles_data table tr.article>td:first-child {
padding-left:10px;
#articles_data span {
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;
line-height:35px;
background-color:#c8102e;
font-size:16px;
color:#FFF;
font-weight:bold;
}
#articles_data table tr.category>td:first-child {
#articles_data div.category>span:first-child {
padding-left:20px;
}

View file

@ -71,8 +71,10 @@ class Config {
* A model subclasses {@link Models.ModelObject}.<br>
* A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.<br>
* A model to manage ModelObject lists
* {@link Models.ModelList}.<br>
* A model to manage ModelObject forests
* {@link Models.ModelForest}.<br>
* A ModelObject that can be fetched through API
* {@link Models.APIModelForest}.<br>
* These classes should not be used directly.
* <br><br>
*
@ -81,7 +83,9 @@ class Config {
* {@link Models.Checkout} (partial).
* <br>
* Models without API support:
* {@link Models.Statement}.
* {@link Models.Statement},
* {@link Models.ArticleCategory},
* {@link Models.Article}.
*
* @namespace Models
*/
@ -107,13 +111,6 @@ class ModelObject {
*/
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.
* @param {Object} [data={}] - data to store in instance
@ -171,15 +168,12 @@ class ModelObject {
}
/**
* Compare function between two instances of the model
* @abstract
* @param {a} Models.ModelObject
* @param {b} Models.ModelObject
* Returns a string value for the model, to use in comparisons
* @see {@link Models.TreeNode.compare|TreeNode.compare}
*/
static compare(a, b) {
return a.id - b.id ;
comparevalue() {
return this.id.toString();
}
}
@ -493,13 +487,6 @@ class ArticleCategory extends ModelObject {
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}
*/
@ -511,8 +498,8 @@ class ArticleCategory extends ModelObject {
* Comparison function between ArticleCategory model instances.
* @see {@link Models.ModelObject.compare|ModelObject.compare}
*/
static compare(a, b) {
return a.name.localeCompare(b.name);
comparevalue() {
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}
*/
@ -562,8 +542,8 @@ class Article extends ModelObject {
* Comparison function between Article model instances.
* @see {@link Models.ModelObject.compare|ModelObject.compare}
*/
static compare(a, b) {
return a.name.localeCompare(b.name);
comparevalue() {
return this.name;
}
// Take care of 'price' type
@ -757,136 +737,148 @@ class Operation extends ModelObject {
/**
* Simple {@link Models.ModelObject} list.
* Node for ModelForest object
* @memberof Models
*/
class ModelList {
/**
* Nested structure of the list
* @abstract
* @type {Models.ModelObject[]}
*/
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;
});
class TreeNode {
constructor(type, content) {
this.type = type;
this.content = content;
this.parent = null;
this.children = [];
}
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
* @param {Object[]} [datalist=[]]
*/
constructor(datalist) {
this.data = {};
this.from(datalist || []);
}
/**
* Fetches an object from the instance data, or creates it if
* it does not exist yet.<br>
* Parent objects are created recursively if needed.
* @param {number} depth depth on the nested structure of the list
* If direction >= 0, parent objects are created recursively.
* If direction <= 0, child objects are created recursively.
* @param {Object} data
* @param {number} direction
*/
get_or_create(depth, data) {
var model = this.constructor.models[depth];
var name = model.verbose_name ;
var existing = this.data[name].find(function (v){
return v.id === data['id'] ;
}) ;
get_or_create(data, direction) {
var model = this.constructor.models[data.type];
var existing = this.find(data.type, data.content.id);
if (existing) {
return existing;
}
if (depth == this.constructor.models.length-1) {
var created = new model(data) ;
this.data[name].push(created);
return created ;
} else {
var par_name = this.constructor.models[depth+1]
.verbose_name;
var content = new this.constructor.models[data.type](data.content);
var node = new TreeNode(data.type, content);
var par_data = data[par_name];
var created = new model(data);
var parnt = this.get_or_create(depth+1, par_data);
created[par_name] = parnt;
this.data[name].push(created);
return created ;
if (direction <= 0) {
if (data.parent) {
var parent = this.get_or_create(data.parent, -1);
node.parent = parent;
parent.children.push(node);
} else {
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
* the lowest level Models.ModelObject in {@link Models.ModelList#models|models}.<br>
* Resets then populates the instance with the given data.
* @param {Object[]} datalist
*/
from(datalist) {
for (let key of this.constructor.names) {
this.data[key] = [];
}
this.roots = [];
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() {
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.
* @param {Models.ModelObject} elt
* @param {Models.TreeNode} node
* @param {Object} templates Templates to render each model
* @param {Object} [options] Options for element render method
*/
render_element(elt, templates, options) {
var name = elt.constructor.verbose_name;
var depth = this.constructor.names.indexOf(name);
// Allows for more granular template specification
var template = templates[elt.type] || templates[name];
render_element(node, templates, options) {
var template = templates[node.type];
var options = options || {} ;
if (depth == -1) {
return $();
} 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);
var $container = $('<div></div>');
$container.attr('id', node.type+'-'+node.content.id);
//TODO: less dirty
var $container = $('<div></div>');
var $elt = elt.display($(template), options);
$elt.attr('data-'+name+'-id', elt.id);
$container.append($elt);
var $rendered = node.content.display($(template), options);
$container.append($rendered);
for (let child of children) {
$container.append(this.render_element(child, templates, options));
}
//TODO: better sorting control
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
*/
display($container, templates, options) {
var root_model = this.constructor.models[this.constructor.models.length-1];
var roots = this.data[root_model.verbose_name];
roots.sort(root_model.compare);
this.roots.sort(TreeNode.compare);
for (let root of roots) {
for (let root of this.roots) {
$container.append(this.render_element(root, templates, options));
}
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.
* @param {class} model
* @param {Object} props Properties to match
*/
find(model, props) {
if (this.constructor.models.indexOf(model) == -1) {
return undefined ;
find(type, id) {
var result = null;
function callback(node) {
if (node.type === type && node.content.id == id)
result = node ;
}
return this.data[model.verbose_name].find(function(v) {
for (let key in props) {
if (v[key] !== props[key])
return false;
}
return true;
});
this.traverse(callback);
return result ;
}
}
/**
* Describes a model list that can be filled through API.
* @extends Models.ModelList
* @extends Models.ModelForest
* @memberof Models
*/
class APIModelList extends ModelList {
class APIModelForest extends ModelForest {
/**
* 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
* {@link Models.APIModelList#url_model}.
* {@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.
@ -970,24 +971,26 @@ class APIModelList extends ModelList {
/**
* ArticleList model. Can be accessed through API.
* @extends Models.APIModelList
* @extends Models.APIModelForest
* @memberof Models
*/
class ArticleList extends APIModelList {
class ArticleList extends APIModelForest {
/**
* Default structure for ArticleList instances
* @default <tt>[Article, ArticleCategory]</tt>
* @see {@link Models.ModelList.models|ModelList.models}
* @abstract
* @default <tt>{'article': Article,
'category': ArticleCategory}</tt>
*/
static get models() {
return [Article, ArticleCategory];
return {'article': Article,
'category': ArticleCategory};
}
/**
* Default url to get ArticleList data
* @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() {
return Urls['kfet.kpsul.articles_data']();

View file

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

View file

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

View file

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