Added Soft remove and a character asset parenting system

This commit is contained in:
Thaum
2015-03-18 08:59:02 +00:00
parent 06b0ad7eba
commit 3656a0b66f
24 changed files with 215 additions and 121 deletions

View File

@@ -18,4 +18,4 @@ aldeed:autoform
conielo:autoform-polymer-paper
msavin:mongol
matb33:collection-hooks
sewdn:collection-behaviours
zimme:collection-softremovable

View File

@@ -15,6 +15,7 @@ blaze-tools@1.0.3
boilerplate-generator@1.0.3
callback-hook@1.0.3
check@1.0.5
coffeescript@1.0.6
conielo:autoform-polymer-paper@0.1.1
dburles:collection-helpers@1.0.2
dburles:mongo-collection-instances@0.3.3
@@ -67,7 +68,6 @@ retry@1.0.3
routepolicy@1.0.5
service-configuration@1.0.4
session@1.1.0
sewdn:collection-behaviours@0.2.0
sha@1.0.3
spacebars@1.0.6
spacebars-compiler@1.0.5
@@ -79,3 +79,5 @@ underscore@1.0.3
url@1.0.4
webapp@1.2.0
webapp-hashing@1.0.3
zimme:collection-behaviours@1.0.3
zimme:collection-softremovable@1.0.3

View File

@@ -27,5 +27,8 @@ Schemas.Action = new SimpleSchema({
Actions.attachSchema(Schemas.Action);
Actions.attachBehaviour('softRemovable');
makeChild(Actions);
Actions.allow(CHARACTER_SUBSCHEMA_ALLOW);
Actions.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -49,10 +49,8 @@ Schemas.Attack = new SimpleSchema({
allowedValues: ["editable", "feature", "class", "buff", "equipment", "racial", "inate"]
},
//the id of the feature, buff or item that created this effect
sourceId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true
parent: {
type: Schemas.Parent
},
color: {
type: String,
@@ -67,5 +65,8 @@ Schemas.Attack = new SimpleSchema({
Attacks.attachSchema(Schemas.Attack);
Attacks.attachBehaviour('softRemovable');
makeChild(Attacks); //children of lots of things
Attacks.allow(CHARACTER_SUBSCHEMA_ALLOW);
Attacks.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -20,11 +20,8 @@ Schemas.Buff = new SimpleSchema({
Buffs.attachSchema(Schemas.Buff);
Buffs.before.remove(function (userId, buff) {
Effects.find({sourceId: buff._id, type: "buff"}).forEach(function(effect){
Effects.remove(effect._id);
});
});
Buffs.attachBehaviour('softRemovable');
makeParent(Buffs); //parents of effects and attacks
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -458,28 +458,3 @@ Characters.deny({
return _.contains(fields, 'owner');
}
});
CHARACTER_SUBSCHEMA_ALLOW = {
// the user must be logged in, and the user must be a writer of the character
insert: function (userId, doc) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
update: function (userId, doc, fields, modifier) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
remove: function (userId, doc) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
fetch: ["charId"]
};
CHARACTER_SUBSCHEMA_DENY = {
update: function (userId, docs, fields, modifier) {
// can't change character
return _.contains(fields, 'charId');
},
fetch: ["charId"]
};

View File

@@ -21,5 +21,8 @@ Schemas.Class = new SimpleSchema({
Classes.attachSchema(Schemas.Class);
Classes.attachBehaviour('softRemovable');
makeParent(Classes); //parents of effects and attacks
Classes.allow(CHARACTER_SUBSCHEMA_ALLOW);
Classes.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -35,11 +35,9 @@ Schemas.Effect = new SimpleSchema({
defaultValue: "editable",
allowedValues: ["editable", "feature", "class", "buff", "equipment", "racial", "inate"]
},
//the id of the feature, buff or item that created this effect
sourceId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true
//the thing that created this effect
parent: {
type: Schemas.Parent
},
//which stat the effect is applied to
stat: {
@@ -91,5 +89,8 @@ Characters.after.insert(function (userId, char) {
}
});
Effects.attachBehaviour('softRemovable');
makeChild(Effects); //children of lots of things
Effects.allow(CHARACTER_SUBSCHEMA_ALLOW);
Effects.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -21,5 +21,7 @@ Schemas.Experience = new SimpleSchema({
Experiences.attachSchema(Schemas.Experience);
Experiences.attachBehaviour('softRemovable');
Experiences.allow(CHARACTER_SUBSCHEMA_ALLOW);
Experiences.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -22,20 +22,8 @@ Features.helpers({
}
});
//Delete effects where this the removed feature is source
Features.before.remove(function (userId, feature) {
Effects.find({sourceId: feature._id, type: "feature"}).forEach(function(effect){
Effects.remove(effect._id);
});
});
//keep the effects up to date with enabled state
Features.after.update(function (userId, feature, fieldNames, modifier, options) {
var enabled = feature.enabled !== "disabled";
Effects.find({sourceId: feature._id, type: "feature"}).forEach(function(effect){
Effects.update(effect._id, { $set: {charId: feature.charId, enabled: enabled, name: feature.name} });
});
}, {fetchPrevious: false});
Features.attachBehaviour('softRemovable');
makeParent(Features); //parents of effects and attacks
Features.allow(CHARACTER_SUBSCHEMA_ALLOW);
Features.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -9,5 +9,7 @@ Schemas.Note = new SimpleSchema({
Notes.attachSchema(Schemas.Note);
Notes.attachBehaviour('softRemovable');
Notes.allow(CHARACTER_SUBSCHEMA_ALLOW);
Notes.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -15,15 +15,12 @@ Schemas.Proficiency = new SimpleSchema({
defaultValue: "editable",
allowedValues: ["editable", "feature", "buff", "equipment", "inate"]
},
//the id of the feature, buff or item that created this effect
sourceId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true
},
});
Proficiencies.attachSchema(Schemas.Proficiency);
Proficiencies.attachBehaviour('softRemovable');
makeChild(Proficiencies);
Proficiencies.allow(CHARACTER_SUBSCHEMA_ALLOW);
Proficiencies.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -23,15 +23,8 @@ SpellLists.helpers({
}
});
SpellLists.before.remove(function (userId, list) {
if(Meteor.isServer){
Spells.remove({listId: list._id});
} else {
Spells.find({listId: list._id}).forEach(function(spell){
Spells.remove(spell._id);
});
}
});
SpellLists.attachBehaviour('softRemovable');
makeParent(SpellLists); //parents of spells
SpellLists.allow(CHARACTER_SUBSCHEMA_ALLOW);
SpellLists.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -2,7 +2,6 @@ Spells = new Mongo.Collection("spells");
Schemas.Spell = new SimpleSchema({
charId: {type: String, regEx: SimpleSchema.RegEx.Id},
listId: {type: String, regEx: SimpleSchema.RegEx.Id},
prepared: {type: String, defaultValue: "unprepared", allowedValues: ["prepared","unprepared","always"]},
name: {type: String, trim: false},
description: {type: String, optional: true, trim: false},
@@ -21,5 +20,8 @@ Schemas.Spell = new SimpleSchema({
Spells.attachSchema(Schemas.Spell);
Spells.attachBehaviour('softRemovable');
makeChild(Spells); //children of spell lists
Spells.allow(CHARACTER_SUBSCHEMA_ALLOW);
Spells.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -19,4 +19,4 @@ Schemas.DeathSave = new SimpleSchema({
type: Boolean,
defaultValue: false
}
});
});

View File

@@ -30,14 +30,7 @@ Containers.helpers({
}
});
Containers.before.remove(function (userId, container) {
if(Meteor.isServer){
Items.remove({container: container._id});
} else {
Items.find({container: container._id}).forEach(function(item){
Items.remove(item._id);
});
}
});
Containers.attachBehaviour('softRemovable');
makeParent(Containers); //parents of items
Containers.allow(CHARACTER_SUBSCHEMA_ALLOW);

View File

@@ -4,7 +4,6 @@ Schemas.Item = new SimpleSchema({
name: {type: String, defaultValue: "New Item", trim: false},
plural: {type: String, optional: true, trim: false},
description:{type: String, optional: true, trim: false},
container: {type: String, regEx: SimpleSchema.RegEx.Id}, //id of container it is normally stowed in
charId: {type: String, regEx: SimpleSchema.RegEx.Id}, //id of owner
quantity: {type: Number, min: 0, defaultValue: 1},
weight: {type: Number, min: 0, defaultValue: 0, decimal: true},
@@ -14,8 +13,8 @@ Schemas.Item = new SimpleSchema({
defaultValue: "none",
allowedValues: ["none", "head", "armor", "arms", "hands", "held", "feet"]
},
equipped: {type: Boolean, defaultValue: false},
color: {type: String, allowedValues: _.pluck(colorOptions, "key"), defaultValue: "q"}
enabled: {type: Boolean, defaultValue: false},
color: {type: String, allowedValues: _.pluck(colorOptions, "key"), defaultValue: "q"}
});
Items.attachSchema(Schemas.Item);
@@ -36,24 +35,8 @@ Items.helpers({
}
});
//remove effects and attacks if their item source is removed
Items.before.remove(function (userId, item) {
Effects.find({sourceId: item._id, type: "equipment"}).forEach(function(effect){
Effects.remove(effect._id);
});
Attacks.find({sourceId: item._id, type: "equipment"}).forEach(function(attack){
Attacks.remove(attack._id);
});
});
//keep the effects and attacks on the correct character and enabled when equipped
Items.after.update(function (userId, item, fieldNames, modifier, options) {
Effects.find({sourceId: item._id, type: "equipment"}).forEach(function(effect){
Effects.update(effect._id, { $set: {charId: item.charId, enabled: item.equipped, name: item.name} });
});
Attacks.find({sourceId: item._id, type: "equipment"}).forEach(function(attack){
Attacks.update(attack._id, { $set: {charId: item.charId, enabled: item.equipped, name: item.name} });
});
}, {fetchPrevious: false});
Items.attachBehaviour('softRemovable');
makeChild(Items); //children of containers
makeParent(Items); //parents of effects and attacks
Items.allow(CHARACTER_SUBSCHEMA_ALLOW);

View File

@@ -0,0 +1,24 @@
CHARACTER_SUBSCHEMA_ALLOW = {
// the user must be logged in, and the user must be a writer of the character
insert: function (userId, doc) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
update: function (userId, doc, fields, modifier) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
remove: function (userId, doc) {
var char = Characters.findOne( doc.charId, { fields: {owner: 1, writers: 1} } );
return ( userId && char.owner === userId || _.contains(char.writers, userId) );
},
fetch: ["charId"]
};
CHARACTER_SUBSCHEMA_DENY = {
update: function (userId, docs, fields, modifier) {
// can't change character
return _.contains(fields, 'charId');
},
fetch: ["charId"]
};

View File

@@ -0,0 +1,112 @@
var childSchema = new SimpleSchema({
parent: { type: Object },
'parent.collection': { type: String },
'parent.id': { type: String, regEx: SimpleSchema.RegEx.Id }
});
var joinWithDefaultKeys = function(keys){
var defaultKeys = [
'charId',
'enabled',
'removed',
'removedAt',
'removedBy',
'restoredAt',
'restoredBy'
];
return _.union(keys, defaultKeys);
}
var childCollections = [];
makeChild = function(collection, inheritedKeys){
collection.inheritedKeys = joinWithDefaultKeys(inheritedKeys);
collection.helpers({
//returns the parent even if it's removed
getParent: function(){
var parentCol = Meteor.isClient?
window[this.parent.collection] : global[this.parent.collection];
if (parentCol)
return parentCol.findOne(this.parent.id, {removed: true});
},
getParentCollection: function(){
return Meteor.isClient?
window[this.parent.collection] : global[this.parent.collection];
}
});
//when we change parents, inherit its properties
collection.after.update(function (userId, doc, fieldNames, modifier, options) {
if(modifier && modifier.$set && modifier.$set.parent){
var parent = doc.getParent();
if(!parent) throw new Meteor.Error('Parenting Error',
'Document\'s parent does not exist');
var handMeDowns = _.pick(parent, collection.inheritedKeys);
collection.update(doc._id, {$set: handMeDowns});
}
});
collection.attachSchema(childSchema);
childCollections.push(collection);
};
makeParent = function(collection, donatedKeys){
collection.donatedKeys = joinWithDefaultKeys(donatedKeys);
//after changing, push the changes to all children
collection.after.update(function (userId, doc, fieldNames, modifier, options) {
if(!modifier) return;
modifier = _.pick(modifier, ['$set', '$unset']);
modifier.$set = _.pick(modifier.$set, donatedKeys);
modifier.$unset = _.pick(modifier.$unset, donatedKeys);
doc = _.pick(doc, ['_id','charId']);
Meteor.call('updateChildren', doc, modifier);
});
collection.after.remove(function (userId, doc) {
doc = _.pick(doc, ['_id','charId']);
Meteor.call('removeChildren', doc);
});
};
var checkPermission = function(userId, charId){
var char = Characters.findOne( charId, { fields: {owner: 1, writers: 1} } );
if(!char)
throw new Meteor.Error('Access Denied',
'Character '+charId+' does not exist');
if (!userId)
throw new Meteor.Error('Access Denied',
'No UserId set when trying to update character asset.');
if (char.owner !== userId && !_.contains(char.writers, userId))
throw new Meteor.Error('Access Denied',
'Not permitted to update assets of this character.');
return true;
};
Meteor.methods({
updateChildren: function (parent, modifier) {
check(parent, {_id: String, charId: String});
check(modifier, Object);
checkPermission(this.userId, parent.charId);
_.each(childCollections, function(collection){
collection.update(
{charId: parent.charId, 'parent.id': parent._id},
modifier,
{multi: true}
);
});
},
removeChildren: function (parent) {
check(parent, {_id: String, charId: String});
checkPermission(this.userId, parent.charId);
_.each(childCollections, function(collection){
collection.remove(
{charId: parent.charId, 'parent.id': parent._id}
);
});
}
});

View File

@@ -1,4 +1,10 @@
Meteor.publish("characterList",function(userId){
if(!userId) return;
return Characters.find({$or: [ {readers: userId}, {writers: userId}, {owner: userId} ] });
return Characters.find({
$or: [
{readers: userId},
{writers: userId},
{owner: userId}
]
});
});

View File

@@ -1,19 +1,29 @@
Meteor.publish("singleCharacter", function(characterId, userId){
//TODO check if this characer can be viewed by this user
return [
Characters.find({_id: characterId}),
Actions.find({charId: characterId}),
Attacks.find({charId: characterId}),
Classes.find({charId: characterId}),
Containers.find({charId: characterId}),
Effects.find({charId: characterId}),
Experiences.find({charId: characterId}),
Features.find({charId: characterId}),
Items.find({charId: characterId}),
Notes.find({charId: characterId}),
Spells.find({charId: characterId}),
SpellLists.find({charId: characterId}),
TemporaryHitPoints.find({charId: characterId}),
];
if(
Characters.findOne({
_id: characterId,
$or: [
{readers: userId},
{writers: userId},
{owner: userId}
]
})
){
return [
Characters.find({_id: characterId}),
//get all the assets for this character including soft deleted ones
Actions.find ({charId: characterId}, {removed: true}),
Attacks.find ({charId: characterId}, {removed: true}),
Classes.find ({charId: characterId}, {removed: true}),
Containers.find ({charId: characterId}, {removed: true}),
Effects.find ({charId: characterId}, {removed: true}),
Experiences.find ({charId: characterId}, {removed: true}),
Features.find ({charId: characterId}, {removed: true}),
Items.find ({charId: characterId}, {removed: true}),
Notes.find ({charId: characterId}, {removed: true}),
Spells.find ({charId: characterId}, {removed: true}),
SpellLists.find ({charId: characterId}, {removed: true}),
TemporaryHitPoints.find({charId: characterId}, {removed: true}),
];
}
});