/**
* All models -- volume, party, session, record, etc.
* @namespace modelService
*/
'use strict';
app.factory('modelService', [
'$q', '$cacheFactory', '$play', 'routerService', 'constantService', 'Segment',
function ($q, $cacheFactory, $play, router, constants, Segment) {
///////////////////////////////// Model: common base class and utils
function resData(res) {
return res.data;
}
function Model(init) {
this.init(init);
}
/* map of fields to to true (static, missingness is significant) or false (update when present) */
Model.prototype.fields = {
id: true,
permission: false,
};
Model.prototype.init = function (init) {
var fields = this.fields;
for (var f in fields) {
if (f in init)
this[f] = init[f];
else if (fields[f])
delete this[f];
}
};
Model.prototype.update = function (init) {
if (typeof init !== 'object')
return this;
if (this.hasOwnProperty('id') && init.id !== this.id)
throw new Error("update id mismatch");
this.init(init);
return this;
};
Model.prototype.clear = function (/*f...*/) {
for (var i = 0; i < arguments.length; i ++)
if (arguments[i] in this)
delete this[arguments[i]];
};
function hasField(obj, opt) {
return obj && opt in obj && (!obj[opt] || typeof obj[opt] !== 'object' || !obj[opt]._PLACEHOLDER);
}
/**
* determine whether the given object satisfies all the given dependency options already.
* returns the missing options, or null if nothing is missing.
* @interface modelService/checkOptions
*/
function checkOptions(obj, options) {
var opts = {};
var need = obj ? null : opts;
if (Array.isArray(options)) {
for (var i = 0; i < options.length; i ++)
if (!hasField(obj, options[i])) {
opts[options[i]] = true;
need = opts;
}
}
else if (!obj)
return options || opts;
else if (options)
_.each(options, function(v, o){
if (v || !hasField(obj, o)) {
opts[o] = v;
need = opts;
}
});
return need;
}
function modelCache(obj, name, size) {
obj.prototype = Object.create(Model.prototype);
obj.prototype.constructor = obj;
obj.prototype.class = name;
var opts = {};
if (size)
opts.number = size;
obj.cache = $cacheFactory(name, opts);
obj.clear = function (/*id...*/) {
if (arguments.length)
for (var i = 0; i < arguments.length; i ++)
obj.cache.remove(arguments[i]);
else
obj.cache.removeAll();
};
obj.poke = function (x) {
return obj.cache.put(x.id, x);
};
}
/**
* delegate the given (missing) fields on instances of obj to the sub-object sub,
* but allow assignments to work directly as usual.
* @interface modelService/delegate
*/
function delegate(obj, sub /*, field... */) {
function descr(f) {
return {
get: function () {
var s = this[sub];
return s && s.hasOwnProperty(f) ? s[f] : undefined;
},
set: function (v) {
Object.defineProperty(this, f, {
configurable: true,
enumerable: true,
writable: true,
value: v
});
}
};
}
for (var i = 2; i < arguments.length; i ++) {
var f = arguments[i];
Object.defineProperty(obj.prototype, f, descr(f));
}
}
///////////////////////////////// Party
function Party(init) {
Model.call(this, init);
}
modelCache(Party, 'party', 256);
Party.prototype.fields = {
id: true,
permission: false,
sortname: true,
prename: true,
orcid: true,
affiliation: true,
email: true,
institution: true,
url: true,
authorization: false,
};
Object.defineProperty(Party.prototype, 'name', {
get: function () {
return (this.prename ? this.prename + ' ' : '') + this.sortname;
}
});
/**
* Initialize party with the following properties: <br />
* <blockquote>id <br />
* permission <br />
* sortname <br />
* prename <br />
* orcid <br />
* affiliation <br />
* email <br />
* institution <br />
* url <br />
* authorization </blockquote>
* @interface modelService/Party/init
*/
Party.prototype.init = function (init) {
Model.prototype.init.call(this, init);
if ('access' in init)
this.access = volumeMakeSubArray(init.access);
if ('volumes' in init)
this.volumes = volumeMakeArray(init.volumes);
if ('parents' in init)
this.parents = partyMakeSubArray(init.parents);
if ('children' in init)
this.children = partyMakeSubArray(init.children);
if ('comments' in init)
this.comments = commentMakeArray(null, init.comments);
};
Party.peek = function (id) {
return id === Login.user.id && Login.user || Party.cache.get(id);
};
function partyMake(init) {
var p = Party.peek(init.id);
return p ? p.update(init) : Party.poke(new Party(init));
}
function partyMakeSubArray(l) {
for (var i = 0; i < l.length; i ++)
l[i].party = partyMake(l[i].party);
return l;
}
function partyMakeArray(l) {
if (l) for (var i = 0; i < l.length; i ++)
l[i] = partyMake(l[i]);
return l;
}
function partyGet(id, p, options) {
if ((options = checkOptions(p, options)))
return router.http(id == Login.user.id ? // may both be undefined (id may be string)
router.controllers.getProfile :
router.controllers.getParty,
id, options)
.then(function (res) {
return p ? p.update(res.data) : Party.poke(new Party(res.data));
});
else
return $q.successful(p);
}
/**
* Get Party
* @interface modelService/Party/get
*/
Party.get = function (id, options) {
var p = Party.peek(id);
if (!p)
return Party.cache.put(id, partyGet(id, p, options));
if (p instanceof Party)
return partyGet(id, p, options);
if (!options)
return p;
return p.then(function (p) {
return partyGet(id, p, options);
});
};
Party.prototype.get = function (options) {
return partyGet(this.id, this, options);
};
/**
* Post Party
* @interface modelService/Party/save
*/
Party.prototype.save = function (data) {
var p = this;
return router.http(router.controllers.postParty, this.id, data)
.then(function (res) {
return p.update(res.data);
});
};
/**
* Search Party
* @interface modelService/Party/search
*/
Party.search = function (data) {
return router.http(router.controllers.getParties, data)
.then(function (res) {
return partyMakeArray(res.data);
});
};
Party.prototype.route = function () {
return router.party([this.id]);
};
Party.prototype.editRoute = function (page, party) {
var params = {};
if (page)
params.page = page;
if (party)
params.party = party.id;
return router.partyEdit([this.id], params);
};
Party.prototype.avatarRoute = function (size, nonce) {
var params = {};
if (nonce)
params.nonce = nonce;
return router.partyAvatar([this.id, size || 56], params);
};
function subPartyUpdate(list, auth) {
if (!list)
return;
auth.party = partyMake(auth.party);
var i = list.findIndex(function (a) {
return a.party.id === auth.party.id;
});
if ('site' in auth || 'member' in auth || auth.individual || auth.children)
if (i >= 0)
list.splice(i, 1, auth);
else
list.push(auth);
else if (i >= 0)
list.splice(i, 1);
}
/**
* Post apply to authorize Party
* @interface modelService/Party/authorizeApply
*/
Party.prototype.authorizeApply = function (target, data) {
var p = this;
return router.http(router.controllers.postAuthorizeApply, this.id, target, data)
.then(function (res) {
subPartyUpdate(p.parents, res.data);
return p;
});
};
Party.prototype.authorizeNotFound = function (data) {
return router.http(router.controllers.postAuthorizeNotFound, this.id, data);
};
/**
* Post authorize Party
* @interface modelService/Party/authorizeSave
*/
Party.prototype.authorizeSave = function (target, data) {
var p = this;
return router.http(router.controllers.postAuthorize, this.id, target, data)
.then(function (res) {
subPartyUpdate(p.children, res.data);
return res.data;
});
};
Party.prototype.authorizeRemove = function (target) {
var p = this;
return router.http(router.controllers.deleteAuthorize, this.id, target)
.then(function (res) {
subPartyUpdate(p.children, res.data);
return p;
});
};
Party.prototype.authorizeRemoveParent = function (target) {
var p = this;
return router.http(router.controllers.deleteAuthorizeParent, this.id, target)
.then(function (res) {
subPartyUpdate(p.parents, res.data);
return p;
});
};
/**
* Get Party activity
* @interface modelService/Party/getActivity
*/
Party.prototype.getActivity = function () {
return router.http(router.controllers.getPartyActivity, this.id)
.then(function (res) {
return makeActivity(res.data);
});
};
///////////////////////////////// Login
function Login(init) {
Party.call(this, init);
}
/**
* Login has the following additional properties: <br />
* <blockquote>csverf <br />
* superuser <br />
* notifications </blockquote>
* @interface modelService/Login
*/
Login.prototype = Object.create(Party.prototype);
Login.prototype.constructor = Login;
Login.prototype.fields = angular.extend({
csverf: false,
superuser: false,
notifications: false,
}, Login.prototype.fields);
Login.user = new Login({id:constants.party.NOBODY});
function loginPoke(l) {
return (Login.user = Party.poke(new Login(l)));
}
loginPoke($play.user);
router.http.csverf = $play.user.csverf;
function loginRes(res) {
var l = res.data;
if (Login.user.id === l.id && Login.user.superuser === l.superuser)
return Login.user.update(l);
$cacheFactory.removeAll();
router.http.csverf = l.csverf;
return loginPoke(l);
}
/**
* Check if the user is logged in
* @interface modelService/Login/isLoggedIn
*/
Login.isLoggedIn = function () {
return Login.user.id !== constants.party.NOBODY;
};
Login.checkAuthorization = function (level) {
return Login.user.authorization >= level;
};
/**
* Check the user's permission level
* @interface modelService/Login/checkPermission
*/
Model.prototype.checkPermission = function (level) {
return this.permission >= level;
};
/* a little hacky, but to get people SUPER on themselves: */
Login.prototype.checkPermission = function (/*level*/) {
return this.id !== constants.party.NOBODY;
};
Login.isAuthorized = function () {
return Login.isLoggedIn() && Login.checkAuthorization(constants.permission.PUBLIC);
};
Login.prototype.route = function () {
return router.profile();
};
_.each({
get: 'getUser',
login: 'postLogin',
logout: 'postLogout',
}, function(api, f){
Login[f] = function (data) {
return router.http(router.controllers[api], data).then(loginRes);
};
});
/**
* Post the user's info
* @interface modelService/Login/saveAccount
*/
Login.prototype.saveAccount = function (data) {
var p = this;
return router.http(router.controllers.postUser, data)
.then(function (res) {
return p.update(res.data);
});
};
/**
* Post the registrant's info
* @interface modelService/Login/register
*/
Login.register = function (data) {
return router.http(router.controllers.postRegister, data);
};
Login.issuePassword = function (data) {
return router.http(router.controllers.postPasswordReset, data);
};
/**
* Get token to register a user
* @interface modelService/Login/getToken
*/
Login.getToken = function (token, auth) {
return router.http(router.controllers.getLoginToken, token, auth)
.then(resData);
};
Login.passwordToken = function (party, data) {
return router.http(router.controllers.postPasswordToken, party, data)
.then(loginRes);
};
///////////////////////////////// Volume
function Volume(init) {
this.containers = {_PLACEHOLDER:true};
this.records = {_PLACEHOLDER:true};
this.assets = {}; // cache only
Model.call(this, init);
}
modelCache(Volume, 'volume', 8);
Volume.prototype.fields = {
id: true,
permission: false,
name: true,
alias: true,
body: true,
doi: true,
creation: true,
owners: true,
citation: false,
links: false,
funding: false,
tags: false,
state: false,
publicaccess: false,
publicsharefull: false,
suggested_mapping: false,
csv_upload_id: false,
column_samples: false,
columns_firstvals: false,
// consumers: false,
// producers: false,
};
/**
* Initialize volume with the following properties: <br />
* <blockquote>id <br />
* permission <br />
* name <br />
* alias <br />
* body <br />
* doi <br />
* creation <br />
* owners <br />
* citation <br />
* links <br />
* funding <br />
* tags <br />
* state <br />
* publicaccess <br />
* publicsharefull <br />
* suggested_mapping <br />
* csv_upload_id <br />
* column_samples <br />
* columns_firstvals </blockquote>
* @interface modelService/Volume/init
*/
Volume.prototype.init = function (init) {
var i, l;
Model.prototype.init.call(this, init);
if ('access' in init) {
this.access = partyMakeSubArray(init.access);
volumeAccessPreset(this, init.publicaccess);
}
if ('records' in init) {
l = init.records;
for (i = 0; i < l.length; i ++)
recordMake(this, l[i]);
delete this.records._PLACEHOLDER;
}
if ('containers' in init) {
l = init.containers;
for (i = 0; i < l.length; i ++)
containerMake(this, l[i]);
delete this.containers._PLACEHOLDER;
}
if ('top' in init) {
this.top = containerMake(this, init.top);
this.top.top = 'global';
}
if ('excerpts' in init)
this.excerpts = excerptMakeArray(this, init.excerpts);
if ('comments' in init)
this.comments = commentMakeArray(this, init.comments);
if ('metrics' in init)
this.metrics = _.groupBy(init.metrics, function (m) { return constants.metric[m].category; });
if ('publicsharefull' in init)
this.publicsharefull = init.publicsharefull
if ('publicaccess' in init)
this.publicaccess = init.publicaccess
if ('suggested_mapping' in init)
this.suggested_mapping = init.suggested_mapping
if ('csv_upload_id' in init)
this.csv_upload_id = init.csv_upload_id
if ('column_samples' in init)
this.column_samples = init.column_samples
if ('columns_firstvals' in init)
this.columns_firstvals = init.columns_firstvals
};
function volumeMake(init) {
var v = Volume.cache.get(init.id);
return v ? v.update(init) : Volume.poke(new Volume(init));
}
function volumeMakeArray(l) {
for (var i = 0; i < l.length; i ++)
l[i] = volumeMake(l[i]);
return l;
}
function volumeMakeSubArray(l) {
for (var i = 0; i < l.length; i ++)
l[i].volume = volumeMake(l[i].volume);
return l;
}
function volumeGet(id, v, options) {
if ((options = checkOptions(v, options))) {
var q = router.http(router.controllers.getVolume,
id, options).then(function (res) {
return v ? v.update(res.data) : Volume.poke(new Volume(res.data));
});
if (!v)
Volume.poke(q);
return q;
} else
return $q.successful(v);
}
/**
* Get volume
* @interface modelService/Volume/get
*/
Volume.get = function (id, options) {
var p = Volume.cache.get(id);
if (!p)
return Volume.cache.put(id, volumeGet(id, p, options));
if (p instanceof Volume)
return volumeGet(id, p, options);
if (!options)
return p;
return p.then(function (p) {
return volumeGet(id, p, options);
});
};
Volume.prototype.get = function (options) {
return volumeGet(this.id, this, options);
};
/**
* Post volume
* @interface modelService/Volume/save
*/
Volume.prototype.save = function (data) {
var v = this;
return router.http(router.controllers.postVolume, this.id, data)
.then(function (res) {
return v.update(res.data);
});
};
Volume.prototype.saveLinks = function (data) {
var v = this;
return router.http(router.controllers.postVolumeLinks, this.id, data)
.then(function (res) {
v.clear('links');
return v.update(res.data);
});
};
Volume.prototype.detectcsv = function (data) {
var v = this;
return router.http(router.controllers.detectParticipantCSV, this.id, data)
.then(function (res) {
return v.update(res.data);
});
};
Volume.prototype.matchcsv = function (data) {
var v = this;
return router.http(router.controllers.runParticipantUpload, this.id, data)
.then(function (res) {
return v.update(res.data);
});
};
/**
* Create volume
* @interface modelService/Volume/create
*/
Volume.create = function (data, owner) {
if (owner !== undefined)
data.owner = owner;
return router.http(router.controllers.createVolume, data)
.then(function (res) {
if ((owner = (owner === undefined ? Login.user : Party.peek(owner))))
owner.clear('access', 'volumes');
return volumeMake(res.data);
});
};
/**
* Search volume
* @interface modelService/Volume/search
*/
Volume.search = function (data) {
return router.http(router.controllers.getVolumes, data)
.then(function (res) {
return res.data.map(volumeMake);
});
};
Object.defineProperty(Volume.prototype, 'displayName', {
get: function () {
return this.alias !== undefined ? this.alias : this.name;
}
});
Volume.prototype.route = function () {
return router.volume([this.id]);
};
Volume.prototype.editRoute = function (page, party) {
var params = {};
if (page)
params.page = page;
if (party)
params.party = party.id;
return router.volumeEdit([this.id], params);
};
Volume.prototype.thumbRoute = function (size) {
return router.volumeThumb([this.id, size]);
};
Volume.prototype.zipRoute = function (params) {
return router.volumeZip([this.id], params);
};
Volume.prototype.zipOrigRoute = function (params) {
return router.volumeZipOrig([this.id], params);
};
Volume.prototype.csvRoute = function () {
return router.volumeCSV([this.id]);
};
function volumeAccessPreset(volume, publicaccess) {
publicaccess = publicaccess || false;
if (!volume.access)
return;
var p = [];
var staff = false;
var al = volume.access.filter(function (a) {
var pi = constants.accessPreset.parties.indexOf(a.party.id);
if (pi >= 0)
p[pi] = a.children;
else if (a.party.id === constants.party.STAFF && a.children === constants.accessPreset.staff)
staff = a.children;
else
return true;
});
var pi = constants.accessPreset.findIndex(function (preset) {
return preset.every(function (s, i) {
return preset[i] === (p[i] || 0);
});
});
if (pi >= 0) {
volume.access = al;
volume.accessStaff = staff;
volume.accessPreset = pi;
if (publicaccess === "full") {
volume.accessPreset = 2;
}
}
}
/**
* Post volume access
* @interface modelService/Volume/accessSave
*/
Volume.prototype.accessSave = function (target, data, sharefull) {
var v = this;
sharefull = sharefull || false;
if (typeof data !== 'object')
data = {individual:data, children: data, share_full: sharefull};
return router.http(router.controllers.postVolumeAccess, this.id, target, data)
.then(function (res) {
subPartyUpdate(v.access, res.data);
volumeAccessPreset(v, res.data.share_full);
return v;
});
};
Volume.prototype.accessRemove = function (target) {
return this.accessSave(target, {"delete":true});
};
Volume.prototype.fundingSave = function (funder, data) {
var v = this;
return router.http(router.controllers.postVolumeFunding, this.id, funder, data)
.then(function (res) {
var d = res.data;
if (v.funding) {
var i = v.funding.findIndex(function (f) {
return f.funder.id == d.funder.id;
});
if (i < 0)
i = v.funding.length;
v.funding[i] = d;
}
return v;
});
};
Volume.prototype.fundingRemove = function (funder) {
var v = this;
return router.http(router.controllers.deleteVolumeFunder, this.id, funder)
.then(function (res) {
if (v.funding) {
v.funding = v.funding.filter(function (f) {
return f.funder.id != funder;
});
}
return v.update(res.data);
});
};
Volume.prototype.setVolumeMetrics = function (c, m, on) {
var v = this;
return router.http(m == null ?
on ? router.controllers.addVolumeCategory : router.controllers.deleteVolumeCategory :
on ? router.controllers.addVolumeMetric : router.controllers.deleteVolumeMetric,
this.id, m == null ? c : m)
.then(function (res) {
var d = res.data;
if ('metrics' in v) {
if (m == null)
if (on)
v.metrics[c] = d;
else
delete v.metrics[c];
else {
v.metrics[c].remove(m);
if (on) {
Array.prototype.push.apply(v.metrics[c], d);
v.metrics[c].sort(function (a, b) { return a - b; });
}
}
}
return d;
});
};
Volume.prototype.getActivity = function () {
var v = this;
return router.http(router.controllers.getVolumeActivity, this.id)
.then(function (res) {
return makeActivity(res.data, v);
});
};
///////////////////////////////// Container/Slot
// This does not handle cross-volume inclusions
function Slot(context, init) {
this.container =
context instanceof Container ? context :
containerPrepare(context, init.container);
if (init)
Model.call(this, init);
}
Slot.prototype = Object.create(Model.prototype);
Slot.prototype.constructor = Slot;
Slot.prototype.class = 'slot';
Slot.prototype.fields = {
release: true,
tags: false,
releases: false,
};
Slot.prototype.clear = function (/*f...*/) {
Model.prototype.clear.apply(this, arguments);
Model.prototype.clear.apply(this.container, arguments);
};
/**
* Slot has the following additional properties: <br />
* <blockquote>release <br />
* tags <br />
* assets <br />
* comments <br />
* container <br />
* records <br />
* excerpts <br />
* volume <br />
* releases </blockquote>
* @interface modelService/Slot
*/
function slotInit(slot, init) {
if ('assets' in init) {
var al = init.assets;
slot.assets = {};
for (var ai = 0; ai < al.length; ai ++) {
var a = assetMake(slot.container, al[ai]);
slot.assets[a.id] = a;
}
}
if ('comments' in init)
slot.comments = commentMakeArray(slot.container, init.comments);
if ('records' in init) {
var rl = init.records;
for (var ri = 0; ri < rl.length; ri ++)
rl[ri].record = rl[ri].record ? recordMake(slot.volume, rl[ri].record) : slot.volume.records[rl[ri].id];
slot.records = rl;
}
if ('excerpts' in init)
slot.excerpts = excerptMakeArray(slot.container, init.excerpts);
}
Slot.prototype.init = function (init) {
Model.prototype.init.call(this, init);
this.segment = new Segment(init.segment);
if ('container' in init)
this.container.update(init.container);
if ('volume' in init)
this.volume.update(init.volume);
slotInit(this, init);
};
delegate(Slot, 'container',
'id', 'volume', 'top', 'date', 'name');
delegate(Slot, 'volume',
'permission');
Object.defineProperty(Slot.prototype, 'displayName', {
get: function () {
return constants.message((this.container.top ? (this.container.top === 'global' ? 'global' : 'materials') : 'session') + '.name') + (this.name ? ': ' + this.name : '');
}
});
Slot.prototype.asSlot = function () {
return this.segment.full ? this.container : angular.extend(new Slot(this.container), this);
};
function Container(volume, init) {
this.volume = volume;
volume.containers[init.id] = this;
Slot.call(this, this, init);
}
Container.prototype = Object.create(Slot.prototype);
Container.prototype.constructor = Container;
Container.prototype.fields = angular.extend({
id: false,
_PLACEHOLDER: true,
name: true,
top: true,
date: true,
}, Container.prototype.fields);
Container.prototype.init = function (init) {
Model.prototype.init.call(this, init);
if ('volume' in init)
this.volume.update(init.volume);
if ('container' in init)
this.update(init.container);
slotInit(this, init);
};
Object.defineProperty(Container.prototype, 'segment', {
get: function () {
return Segment.full;
}
});
Container.prototype.remove = function () {
var c = this;
return router.http(router.controllers.deleteContainer, this.id)
.then(function () {
delete c.volume.containers[c.id];
return true;
}, function (res) {
if (res.status == 409) {
c.update(res.data);
return false;
}
return $q.reject(res);
});
};
function containerMake(volume, init) {
var c = volume.containers[init.id];
if (c) {
if (!init._PLACEHOLDER)
c.update(init);
return c;
} else
return new Container(volume, init);
}
Volume.prototype.getContainer = function(id) {
return containerMake(this, {id:id,_PLACEHOLDER:true});
};
function containerPrepare(volume, init) {
if (typeof init == 'number')
return volume.getContainer(init);
return containerMake(volume || volumeMake(init.volume), init);
}
Volume.prototype.getSlot = function (container, segment, options) {
return this.getContainer(parseInt(container, 10)).getSlot(segment, options);
};
Container.prototype.getSlot = function (segment, options) {
var c = this;
if (Segment.isFull(segment))
if ((options = checkOptions(this, options)) || this._PLACEHOLDER)
return router.http(router.controllers.getSlot,
this.id, Segment.format(segment), options)
.then(function (res) {
return c.update(res.data);
});
else return $q.successful(this);
else return router.http(router.controllers.getSlot,
this.id, Segment.format(segment), checkOptions(null, options))
.then(function (res) {
return new Slot(c, res.data);
});
};
/**
* Post slot
* @interface modelService/Slot/save
*/
Slot.prototype.save = function (data) {
var s = this;
if ('release' in data) {
data.release += '';
if (data.release === 'undefined' || data.release == -1)
data.release = '';
}
return router.http(router.controllers.postContainer, this.container.id, this.segment.format(), data)
.then(function (res) {
if ('release' in data) {
s.clear('releases');
s.container.clear('releases');
}
return s.update(res.data);
});
};
Volume.prototype.createContainer = function (data) {
var v = this;
return router.http(router.controllers.createContainer, this.id, data)
.then(function (res) {
return new Container(v, res.data);
});
};
/**
* Post slot record
* @interface modelService/Slot/addRecord
*/
Slot.prototype.addRecord = function (r, seg) {
if (!seg)
seg = this.segment;
var s = this;
return router.http(router.controllers.postRecordSlot, this.container.id, seg.format(), r.id)
.then(function (res) {
if (res.data.measures) {
r.update(res.data);
return;
}
var d = res.data;
d.record = r;
if ('records' in s)
s.records.push(d);
if (s.container !== s && 'records' in s.container)
s.container.records.push(d);
return d;
});
};
/**
* Create slot record
* @interface modelService/Slot/newRecord
*/
Slot.prototype.newRecord = function (c) {
var s = this;
if (c && typeof c === 'object')
c = c.id;
return router.http(router.controllers.createRecord, this.volume.id, {category:c})
.then(function (res) {
var r = new Record(s.volume, res.data);
return s.addRecord(r);
});
};
Slot.prototype.removeRecord = Slot.prototype.moveRecord = function (r, src, dst) {
if (arguments.length < 3) {
dst = null;
if (src == null)
src = this.segment;
}
var s = this;
return router.http(router.controllers.postRecordSlot, this.container.id, Segment.format(dst), r.id, {src: Segment.data(src)})
.then(function (res) {
if (!res.data)
return;
if (res.data.measures) {
r.update(res.data);
return null;
}
var d = new Segment(res.data.segment);
if (s.records) {
var ss = Segment.make(src);
for (var ri = 0; ri < s.records.length; ri ++) {
if (s.records[ri].id === r.id && ss.contains(s.records[ri].segment)) {
if (d.empty)
s.records.splice(ri, 1);
else
s.records[ri].segment = d;
break;
}
}
}
return d;
});
};
Slot.prototype.route = function (params) {
return router.slot([this.volume.id, this.container.id, this.segment.format()], params);
};
Slot.prototype.editRoute = function (params) {
return router.slotEdit([this.volume.id, this.container.id, this.segment.format()], params);
};
Slot.prototype.zipRoute = function () {
return router.slotZip([this.volume.id, this.container.id]);
};
Slot.prototype.zipOrigRoute = function () {
return router.slotOrigZip([this.volume.id, this.container.id]);
};
Slot.prototype.thumbRoute = function (size) {
return router.slotThumb([this.volume.id, this.container.id, this.segment.format(), size]);
};
Container.prototype.getActivity = function () {
var v = this.volume;
return router.http(router.controllers.getContainerActivity, this.id)
.then(function (res) {
return makeActivity(res.data, v);
});
};
///////////////////////////////// Record
function Record(volume, init) {
this.volume = volume;
volume.records[init.id] = this;
Model.call(this, init);
}
Record.prototype = Object.create(Model.prototype);
Record.prototype.constructor = Record;
Record.prototype.class = 'record';
Record.prototype.fields = {
id: true,
category: true,
measures: true,
// slots: false,
};
/**
* Record has the following properties: <br />
* <blockquote>id <br />
* category <br />
* measures <br />
* volume <br />
* permission </blockquote>
* @interface modelService/Record
*/
Record.prototype.init = function (init) {
Model.prototype.init.call(this, init);
if ('volume' in init)
this.volume.update(init.volume);
};
delegate(Record, 'volume',
'permission');
function recordMake(volume, init) {
var r = volume.records[init.id];
return r ? r.update(init) : new Record(volume, init);
}
Volume.prototype.getRecord = function (record) {
if (record instanceof Record)
return $q.successful(record);
if (record in this.records)
return $q.successful(this.records[record]);
var v = this;
return router.http(router.controllers.getRecord, record)
.then(function (res) {
return new Record(v, res.data);
});
};
Volume.prototype.createRecord = function (c) {
var v = this;
return router.http(router.controllers.createRecord, this.id, {category: c})
.then(function (res) {
return new Record(v, res.data);
});
};
Record.prototype.remove = function () {
var r = this;
return router.http(router.controllers.deleteRecord, this.id)
.then(function () {
delete r.volume.records[r.id];
return true;
}, function (res) {
if (res.status == 409) {
r.update(res.data);
return false;
}
return $q.reject(res);
});
};
Record.prototype.measureSet = function (metric, value) {
var r = this;
return router.http(router.controllers.postRecordMeasure, this.id, metric, {datum:value})
.then(function (res) {
return r.update(res.data);
});
};
Object.defineProperty(Record.prototype, 'displayName', {
get: function () {
var cat = constants.category[this.category];
var met = this.volume.metrics[cat.id];
if (!met)
return;
met = constants.metric[met[0]];
if (met.type === 'void')
return met.name;
var val = this.measures[met.id];
if (val)
return cat.name + ' ' + val;
return cat.name;
}
});
///////////////////////////////// AssetSlot
// Generic parent for Asset and AssetSegment
function AssetSlot(init) {
Model.call(this, init);
}
AssetSlot.prototype = Object.create(Slot.prototype);
AssetSlot.prototype.constructor = AssetSlot;
AssetSlot.prototype.class = 'asset-slot';
AssetSlot.prototype.fields = angular.extend({
permission: true,
}, AssetSlot.prototype.fields);
AssetSlot.prototype.init = function (init) {
Slot.prototype.init.call(this, init);
if ('format' in init)
this.format = constants.format[init.format];
};
function assetMake(context, init) {
var v = context.volume || context;
if (typeof init === 'number')
return v.assets[init];
if ('id' in init) {
var a = v.assets[init.id];
return a ? a.update(init) : new Asset(context, init);
} else {
if (init.asset && 'container' in init && !('container' in init.asset))
init.asset.container = init.container;
return new AssetSegment(context, init);
}
}
Volume.prototype.getAsset = function (asset, container, segment, options) {
var v = this;
options = checkOptions(null, options);
return (container === undefined ?
router.http(router.controllers.getAsset, v.id, asset, options) :
router.http(router.controllers.getAssetSegment, v.id, container, Segment.format(segment), asset, options))
.then(function (res) {
return assetMake(v, res.data);
});
};
AssetSlot.prototype.route = function () {
return router.slotAsset([this.volume.id, this.container.id, this.segment.format(), this.id]);
};
AssetSlot.prototype.slotRoute = function () {
var params = {};
params.asset = this.id;
params.select = this.segment.format();
return this.container.route(params);
};
// override Slot's delegation #4144
AssetSlot.prototype.name = undefined;
Object.defineProperty(AssetSlot.prototype, 'icon', {
get: function () {
return '/web/images/filetype/16px/' + this.format.extension + '.svg';
}
});
Object.defineProperty(AssetSlot.prototype, 'displayName', {
get: function () {
return this.name || this.format.name;
}
});
AssetSlot.prototype.inSegment = function (segment) {
segment = this.segment.intersect(segment);
if (this instanceof AssetSegment && segment.equals(this.segment))
return this;
return new AssetSegment(this.asset, {permission:this.permission, segment:segment});
};
AssetSlot.prototype.inContext = function () {
if (this.asset.permission < this.permission)
return this;
return this.asset;
};
AssetSlot.prototype.setExcerpt = function (release) {
var a = this;
if (release === true)
release = undefined;
return router.http(release !== null ? router.controllers.postExcerpt : router.controllers.deleteExcerpt, this.container.id, this.segment.format(), this.id, {release:release})
.then(function (res) {
if (a instanceof Excerpt && 'excerpt' in res.data)
return a.update(res.data);
a.volume.clear('excerpts');
if (a.asset.excerpts)
a.asset.excerpts.remove(a);
return ('excerpt' in res.data) ? excerptMake(a.asset, res.data) : new AssetSegment(a.asset, res.data);
});
};
AssetSlot.prototype.thumbRoute = function (size) {
return this.container ?
router.assetThumb([this.container.id, this.segment.format(), this.id, size]) :
router.rawAssetThumb([this.id, size]);
};
AssetSlot.prototype.downloadRoute = function (inline) {
return this.container ?
router.assetDownload([this.container.id, this.segment.format(), this.id, inline]) :
router.rawAssetDownload([this.id, inline]);
};
///////////////////////////////// Asset
// This usually maps to a SlotAsset, but may be an unlinked Asset
function Asset(context, init) {
if (init.container || context instanceof Container)
Slot.call(this, context, init);
else {
this.volume = context;
Model.call(this, init);
}
this.volume.assets[this.id] = this;
}
Asset.prototype = Object.create(AssetSlot.prototype);
Asset.prototype.constructor = Asset;
Asset.prototype.class = 'asset';
/**
* Asset has the following properties: <br />
* <blockquote>id <br />
* classification <br />
* name <br />
* duration <br />
* pending <br />
* size <br />
* creation </blockquote>
* @interface modelService/Asset
*/
Asset.prototype.fields = angular.extend({
id: true,
classification: true,
name: true,
duration: true,
pending: true,
size: true,
creation: false,
}, Asset.prototype.fields);
Asset.prototype.init = function (init) {
if (!this.container && 'container' in init)
this.container = containerPrepare(this.volume, init.container);
AssetSlot.prototype.init.call(this, init);
};
Object.defineProperty(Asset.prototype, 'asset', {
get: function () {
return this;
}
});
Object.defineProperty(Asset.prototype, 'fullExcerpt', {
get: function () {
var e = this.excerpts;
if (e && e.length === 1 && e[0].full)
return e[0];
}
});
Object.defineProperty(Asset.prototype, 'release', {
get: function () {
var r = this.classification != null ? this.classification : (this.container.release || 0);
var e = this.fullExcerpt;
return e ? Math.max(e.excerpt || 0, r) : r;
}
});
Object.defineProperty(Asset.prototype, 'assumedSegment', {
get: function () {
return this.segment.full ? new Segment(0, this.duration || 0) : this.segment;
}
});
Asset.prototype.checkPermission = function (level) {
var e = this.fullExcerpt;
return (e ? e.permission : this.permission) >= level;
};
Asset.prototype.get = function (options) {
var a = this;
if ((options = checkOptions(a, options)))
return router.http(router.controllers.getAsset, a.id, options)
.then(function (res) {
return a.update(res.data);
});
else
return $q.successful(a);
};
Asset.prototype.save = function (data) {
var a = this;
if ('classification' in data) {
data.classification += '';
if (data.classification === 'undefined' || data.classification == -1)
data.classification = '';
}
return router.http(router.controllers.postAsset, this.id, data)
.then(function (res) {
if ('excerpt' in data) {
a.clear('excerpts');
a.volume.clear('excerpts');
}
return a.update(res.data);
});
};
Asset.prototype.link = function (slot, data) {
if (!data)
data = {};
data.container = slot.container.id;
data.position = slot.segment.l;
return this.save(data);
};
Slot.prototype.createAsset = function (data) {
var s = this;
if (!data)
data = {};
data.container = this.container.id;
if (!('position' in data) && isFinite(this.segment.l))
data.position = this.segment.l;
return router.http(router.controllers.createAsset, this.volume.id, data)
.then(function (res) {
var a = assetMake(s.container, res.data);
if ('assets' in s)
s.assets[a.id] = a;
return a;
});
};
Asset.prototype.replace = function (data) {
var a = this;
return router.http(router.controllers.postAsset, this.id, data)
.then(function (res) {
a.id = res.data.id;
delete a.$$hashKey; // angular hack
a.volume.assets[a.id] = a;
return a.update(res.data);
});
};
Asset.prototype.remove = function () {
var a = this;
return router.http(router.controllers.deleteAsset, this.id)
.then(function (res) {
if (a.container && a.container.assets)
delete a.container.assets[a.id];
return a.update(res.data);
});
};
///////////////////////////////// AssetSegment
// This usually maps to an Excerpt
function AssetSegment(context, init) {
this.asset =
context instanceof Asset ? context :
assetMake(context, init.asset);
AssetSlot.call(this, init);
}
AssetSegment.prototype = Object.create(AssetSlot.prototype);
AssetSegment.prototype.constructor = AssetSegment;
AssetSegment.prototype.class = 'asset-segment';
AssetSegment.prototype.fields = angular.extend({
excerpt: true,
}, AssetSegment.prototype.fields);
AssetSegment.prototype.init = function (init) {
Model.prototype.init.call(this, init);
this.asset.update(init.asset);
this.segment = new Segment(init.segment);
if ('format' in init)
this.format = constants.format[init.format];
};
delegate(AssetSegment, 'asset',
'id', 'container', 'format', 'duration', 'classification', 'name', 'pending');
Object.defineProperty(AssetSegment.prototype, 'release', {
get: function () {
return Math.max(this.excerpt || 0, this.asset.release);
}
});
///////////////////////////////// AssetSegment
function Excerpt(context, init) {
AssetSegment.call(this, context, init);
if (this.asset.excerpts) {
/* this is unfortunate, but can happen. */
var s = this.segment;
var i = this.asset.excerpts.findIndex(function (e) { return e.segment.equals(s); });
if (i >= 0)
this.asset.excerpts[i] = this;
else
this.asset.excerpts.push(this);
} else
this.asset.excerpts = [this];
}
Excerpt.prototype = Object.create(AssetSegment.prototype);
Excerpt.prototype.constructor = Excerpt;
Excerpt.prototype.class = 'excerpt';
function excerptMake(context, init) {
if (init.asset && 'container' in init && !('container' in init.asset))
init.asset.container = init.container;
var e;
if (context instanceof Asset && context.excerpts && (e = context.excerpts.find(function (e) { return e.segment.equals(init.segment); })))
return e.update(init);
else
return new Excerpt(context, init);
}
function excerptMakeArray(context, l) {
if (l) for (var i = 0; i < l.length; i ++)
l[i] = excerptMake(context, l[i]);
return l;
}
Object.defineProperty(Excerpt.prototype, 'full', {
get: function () {
return this.segment.contains(this.asset.assumedSegment);
}
});
///////////////////////////////// Comment
function Comment(context, init) {
Slot.call(this, context, init);
}
Comment.prototype = Object.create(Slot.prototype);
Comment.prototype.constructor = Comment;
Comment.prototype.class = 'comment';
Comment.prototype.fields = angular.extend({
id: true,
time: true,
text: true,
parents: true
}, Comment.prototype.fields);
Comment.prototype.init = function (init) {
Slot.prototype.init.call(this, init);
if ('who' in init)
this.who = partyMake(init.who);
};
function commentMakeArray(context, l) {
if (l) for (var i = 0; i < l.length; i ++)
l[i] = new Comment(context, l[i]);
return l;
}
Slot.prototype.postComment = function (data, segment, reply) {
if (segment === undefined)
segment = this.segment;
if (arguments.length < 3 && this instanceof Comment)
reply = this.id;
var s = this;
if (reply != null)
data.parent = reply;
return router.http(router.controllers.postComment, this.container.id, segment.format(), data)
.then(function (res) {
s.volume.clear('comments');
s.clear('comments');
return new Comment(s.container, res.data);
});
};
///////////////////////////////// Tag
// no point in a model, really
var Tag = {};
Tag.search = function (query) {
return router.http(router.controllers.getTags, query)
.then(function(res) {
return res.data;
});
};
Tag.top = function () {
return router.http(router.controllers.getTopTags)
.then(function(res) {
return res.data;
});
};
Slot.prototype.setTag = function (tag, vote, keyword, segment) {
if (segment === undefined)
segment = this.segment;
var s = this;
return router.http(router.controllers[vote ? (keyword ? "postKeyword" : "postTag") : (keyword ? "deleteKeyword" : "deleteTag")], this.container.id, segment.format(), tag)
.then(function (res) {
var tag = res.data;
s.volume.clear('tags');
if ('tags' in s)
s.tags[tag.id] = tag;
return tag;
});
};
/////////////////////////////////
function makeActivity (a, volume) {
for (var i = 0; i < a.length; i++) {
if (a[i].party)
a[i].party = partyMake(a[i].party);
if ('segment' in a[i])
a[i].segment = new Segment(a[i].segment);
if (a[i].old && 'segment' in a[i].old)
a[i].old.segment = new Segment(a[i].old.segment);
}
return a;
}
return {
Party: Party,
Login: Login,
Volume: Volume,
Container: Container,
Slot: Slot,
Record: Record,
Asset: Asset,
Comment: Comment,
Tag: Tag,
funder: function (query, all) {
return router.http(router.controllers.getFunders, {query:query,all:all})
.then(resData);
},
cite: function (url) {
return router.http(router.controllers.getCitation, {url:url})
.then(resData);
},
analytic: function () {
return router.http(router.controllers.get, {}, {cache:false});
},
siteActivity: function () {
return router.http(router.controllers.getSiteActivity)
.then(function (res) {
var r = res.data;
var a = r.activity;
for (var i = 0; i < a.length; i ++) {
if ('volume' in a[i])
a[i].volume = volumeMake(a[i].volume);
if ('party' in a[i])
a[i].party = partyMake(a[i].party);
}
return r;
});
}
};
}
]);