diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index 3d8c63f4..371331c0 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -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; } diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js index 5748c3ee..4bbc5c6a 100644 --- a/kfet/static/kfet/js/kfet.api.js +++ b/kfet/static/kfet/js/kfet.api.js @@ -71,8 +71,10 @@ class Config { * A model subclasses {@link Models.ModelObject}.
* A model whose instances can be got from API subclasses * {@link Models.APIModelObject}.
- * A model to manage ModelObject lists - * {@link Models.ModelList}.
+ * A model to manage ModelObject forests + * {@link Models.ModelForest}.
+ * A ModelObject that can be fetched through API + * {@link Models.APIModelForest}.
* These classes should not be used directly. *

* @@ -81,7 +83,9 @@ class Config { * {@link Models.Checkout} (partial). *
* 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 'article_category' - * @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 'article' - * @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.
- * 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}.
+ * 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 = $('
'); + $container.attr('id', node.type+'-'+node.content.id); - //TODO: less dirty - var $container = $('
'); - 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 [Article, ArticleCategory] - * @see {@link Models.ModelList.models|ModelList.models} + * @abstract + * @default {'article': Article, + 'category': ArticleCategory} */ static get models() { - return [Article, ArticleCategory]; + return {'article': Article, + 'category': ArticleCategory}; } /** * Default url to get ArticleList data * @default django-js-reverse('kfet.kpsul.articles_data') - * @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'](); diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index 238a40a6..b2b4c5fa 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -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': '', - 'article' : ''} + this.templates = {'category': '
', + 'article' : '
'} 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': []}; + } } diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 216c7f74..f653ae61 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -131,10 +131,6 @@
- - - -
@@ -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(); }); diff --git a/kfet/views.py b/kfet/views.py index ca750bc3..d341bd68 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -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)