aircox-radiocampus/assets/src/model.js
Chris Tactic 55123c386d #132 | #121: backoffice / dev-1.0-121 (#131)
cfr #121

Co-authored-by: Christophe Siraut <d@tobald.eu.org>
Co-authored-by: bkfox <thomas bkfox net>
Co-authored-by: Thomas Kairos <thomas@bkfox.net>
Reviewed-on: rc/aircox#131
Co-authored-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
Co-committed-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
2024-04-28 22:02:09 +02:00

372 lines
9.9 KiB
JavaScript

/**
* 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]();
}