/** * Return cookie with provided key */ function getCookie(key) { if(document.cookie && document.cookie !== '') { const cookie = document.cookie.split(';') .find(c => c.trim().startsWith(key + '=')) return cookie ? decodeURIComponent(cookie.split('=')[1]) : null; } return null; } /** * CSRF token provided by Django */ var csrfToken = null; /** * Get CSRF token */ export function getCsrf() { if(csrfToken === null) csrfToken = getCookie('csrftoken') return csrfToken; } // TODO: prevent duplicate simple fetch /** * Provide interface used to fetch and manipulate objects. */ export default class Model { /** * Instanciate model with provided data and options. * By default `url` is taken from `data.url_`. */ constructor(data={}, {url=null, ...options}={}) { this.url = url || data.url_; this.options = options; this.commit(data); } get created() { return !this.id } get errors() { return this.data && this.data.__errors__ } /** * Get instance id from its data */ static getId(data) { return 'id' in data ? data.id : data.pk; } /** * Return fetch options */ static getOptions(options) { return { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRFToken': getCsrf(), }, ...options, } } /** * Return model instances for the provided list of model data. * @param {Array} items: array of data * @param {Object} options: options passed down to all model instances */ static fromList(items, options={}) { return items ? items.map(d => new this(d, options)) : [] } /** * Fetch item from server */ static fetch(url, {many=false, ...options}={}, args={}) { options = this.getOptions(options) const request = fetch(url, options).then(response => response.json()); if(many) return request.then(data => { if(!(data instanceof Array)) data = data.results return this.fromList(data, args) }) else return request.then(data => new this(data, {url: url, ...args})); } /** * Fetch data from server. */ fetch(options) { options = this.constructor.getOptions(options) return fetch(this.url, options) .then(response => response.json()) .then(data => this.commit(data)); } /** * Call API action on object. */ action(path, options, commit=false) { options = this.constructor.getOptions(options) const promise = fetch(this.url + path, options); return commit ? promise.then(data => data.json()) .then(data => { this.commit(data); this.data }) : promise; } /** * Set instance's data with provided data. Return None */ commit(data) { this.data = data; this.id = this.constructor.getId(this.data); } /** * Update model data, without reset previous value. * Item is marked as updated. */ update(data) { this.data = {...this.data, ...data} this.id = this.constructor.getId(this.data) this.updated = true } delete() { this.deleted = true } /** * Save instance into localStorage. */ store(key) { window.localStorage.setItem(key, JSON.stringify(this.data)); } /** * Load model instance from localStorage. */ static storeLoad(key) { let item = window.localStorage.getItem(key); return item === null ? item : new this(JSON.parse(item)); } /** * Return true if model instance has no data */ get isEmpty() { return !this.data || Object.keys(this.data).findIndex(k => !!this.data[k] && this.data[k] !== 0) == -1 } /** * Return error for a specific attribute name if any */ error(attr=null) { return attr === null ? this.errors : this.errors && this.errors[attr] } } /** * List of models */ export class Set { constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) { this.items = []; this.model = model; this.url = url; this.unique = unique; this.max = max; this.storeKey = storeKey; for(var item of items) this.push(item, {args: args, save: false}); } //! Return total items count get length() { return this.items.length } //! Return a list of items marked as deleted get deletedItems() { return this.items.filter(i => i.deleted) } //! Return a list of created items get createdItems() { return this.items.filter(i => !i.deleted && !i.id) } //! Return a list of updated items get updatedItems() { return this.items.filter(i => i.updated) } /** * Fetch multiple items from server */ static fetch(model, url, options=null, args=null) { options = model.getOptions(options) return fetch(url, options) .then(response => response.json()) .then(data => (data instanceof Array ? data : data.results) .map(d => new model(d, {url: url, ...args}))) } fetch({url=null, reset=false, ...options}={}, args=null) { url = url || this.url options = this.model.getOptions(options) return fetch(url, options) .then(response => response.json()) .then(data => (data instanceof Array ? data : data.results) .map(d => new this.model(d, {url: url, ...args})) ) .then(data => { if(reset) this.items = data else // TODO: remove duplicate this.items = [...this.items, ...data] return data }) } /** * Commit changes to server. * py-ref: `views.mixin.ListCommitMixin` */ commit(url, {getData=null, fields=null, ...options}={}) { if(!getData && fields) getData = (i) => fields.reduce((r, f) => { r[f] = i.data[f] return r }, {}) const createdItems = this.createdItems const body = { delete: this.deletedItems.map(i => i.id), update: this.updatedItems.map(getData), create: createdItems.map(getData), } if(!body.delete && !body.update && !body.create) return getData = getData || ((i) => i.data); options = this.model.getOptions(options) options.method = "POST" options.body = JSON.stringify(body) return fetch(url, options) .then(response => response.json()) .then(data => { const {created, updated, deleted} = data if(createdItems) this.items = this.items.filter(i => createdItems.indexOf(i) == -1) if(deleted) this.items = this.items.filter(i => deleted.indexOf(i.id) == -1) this.extend(created) this.extend(updated) return data }) } /** * Load list from localStorage */ static storeLoad(model, key, args={}) { let items = window.localStorage.getItem(key); return new this(model, {...args, storeKey: key, items: items ? JSON.parse(items) : []}); } /** * Store list into localStorage */ store() { this.storeKey && window.localStorage.setItem(this.storeKey, JSON.stringify( this.items.map(i => i.data))); } /** * Save item */ save() { this.storeKey && this.store(); } /** * Get item at index */ get(index) { return this.items[index] } /** * Find an item by id or using a predicate function */ find(pred) { return pred instanceof Function ? this.items.find(pred) : this.items.find(x => x.id == pred.id); } /** * Find item index by id or using a predicate function */ findIndex(pred) { return pred instanceof Function ? this.items.findIndex(pred) : this.items.findIndex(x => x.id == pred.id); } extend(items, options) { items.forEach(i => this.push(i, options)) } /** * Add item to set, return index. * If item already exists, replace it. */ push(item, {args={},save=true}={}) { item = item instanceof this.model ? item : new this.model(item, args); let index = -1 if(this.unique && item.id) { index = this.findIndex(item); if(index > -1) this.items[index] = item } if(index == -1) { if(this.max && this.items.length >= this.max) this.items.splice(0,this.items.length-this.max) this.items.push(item) index = this.items.length-1 } save && this.save() return index; } /** * Remove item from set by index */ remove(index, {save=true}={}) { this.items.splice(index,1); save && this.save(); } /** * Clear items, assign new ones */ reset(items=[]) { // TODO: check reactivity this.items = [] for(var item of items) this.push(item) } move(from, to) { if(from >= this.length || to > this.length) throw "source or target index is not in range" const value = this.items[from] this.items.splice(from, 1) this.items.splice(to, 0, value) } } Set[Symbol.iterator] = function () { return this.items[Symbol.iterator](); }