diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js
index 21a67c9c..3caf3662 100644
--- a/kfet/static/kfet/js/kfet.api.js
+++ b/kfet/static/kfet/js/kfet.api.js
@@ -71,7 +71,9 @@ class Config {
* A model subclasses {@link Models.ModelObject}.
* A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.
- * These two classes should not be used directly.
+ * A model to manage ModelObject lists
+ * {@link Models.ModelList}.
+ * These classes should not be used directly.
*
*
* Models with API support:
@@ -105,6 +107,13 @@ 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
@@ -269,6 +278,91 @@ class APIModelObject extends ModelObject {
}
+/**
+ * Simple {@link Models.ModelObject} list.
+ * @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.constructor.verbose_name;
+ });
+ }
+
+ /**
+ * Fetches an object from the object 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
+ * @param {Object} data
+ */
+ get_or_create(depth, data) {
+ var model = this.constructor.models[depth];
+ var name = model.constructor.verbose_name ;
+
+ var existing = this.data[name].find(function (v){
+ return v.id === data['id'] ;
+ }) ;
+
+ if (existing) {
+ return existing;
+ }
+
+ if (depth == this.constructor.models.length) {
+ var created = new model(data) ;
+ return created ;
+ } else {
+ var par_name = this.constructor.models[depth+1]
+ .constructor.verbose_name;
+
+ 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 ;
+ }
+ }
+
+ /**
+ * Resets then populates the instance with the given data, starting from
+ * the lowest level Models.ModelObject in {@link Models.ModelList#models|models}.
+ * @param {Object[]} datalist
+ */
+ from(datalist) {
+
+ for (let key of this.constructor.names) {
+ this.data[key] = [];
+ }
+
+ for (let data of datalist) {
+ this.get_or_create(data, 0);
+ }
+ }
+
+ /**
+ * Removes all Models.ModelObject from the list.
+ */
+ clear() {
+ this.from([]);
+ }
+
+}
+
/**
* Account model. Can be accessed through API.
* @extends Models.APIModelObject