/**
* @module mongodb/models/advertiser
*/
var mongoose = require('mongoose');
var deepPopulate = require('mongoose-deep-populate');
var Schema = mongoose.Schema;
var util = require('util');
var _ = require('underscore');
var mongooseApiQuery = require('mongoose-api-query');
var TreeDocument = require('./lib/treeDocument').TreeDocument;
// ---- Advertisers Collection Models ---- //
/**
* Mongoose Schema representing Advertiser Creatives
*
* @class
* @type {Schema}
*/
var creativeSchema = exports.creativeSchema = new Schema({
name: {type: String, required: true},
active: {type: Boolean, required: true, default: true},
type: String,
tstamp: {type: Date, default: Date.now},
h: {type: Number, required: true},
w: {type: Number, required: true},
retina: {type: Boolean, required: true, default: false},
url: {type: String, required: true },
click_url: {type: String, required: true},
tag: String,
weight: {type: Number, min: 0, max: 5, required: true, default: 1}
});
creativeSchema.plugin(deepPopulate, {});
creativeSchema.plugin(mongooseApiQuery);
var Creative = mongoose.model('Creative', creativeSchema);
// Virtual field to retrieve secure URL
creativeSchema.virtual('secureUrl').get(function(){
if (this.url){
return this.url.replace('http://', 'https://');
}
});
creativeSchema.set('toObject', { virtuals: true });
creativeSchema.set('toJSON', { virtuals: true });
function heightValidator(val){
var valid = true;
this.creatives.forEach(function(creative){
valid = creative.h == val;
});
return valid;
}
function widthValidator(val){
var valid = true;
this.creatives.forEach(function(creative){
valid = creative.w == val;
});
return valid;
}
/**
* Mongoose Schema representing Advertiser CreativeGroups
*
* @class
* @type {Schema}
*/
var creativeGroupSchema = new Schema({
name: String,
tstamp: {type: Date, default: Date.now },
active: {type: Boolean, required: true, default: true},
h: {type: Number, required: true, validate: heightValidator },
w: {type: Number, required: true, validate: widthValidator },
creatives: [creativeSchema]
});
creativeGroupSchema.plugin(deepPopulate, {});
creativeGroupSchema.plugin(mongooseApiQuery);
/**
* Randomly choose creative from this creative group according to
* respective creative weights.
*
* @returns {Creative}
*/
creativeGroupSchema.methods.getWeightedRandomCreative = function(){
var creatives = [];
this.creatives.forEach(function(item){
// filter on active flag
if (item.active){
for (var i= 0; i < item.weight; i++){
creatives.push(item);
}
}
});
return creatives[Math.floor(Math.random()*creatives.length)]
};
var CreativeGroup = mongoose.model('CreativeGroup', creativeGroupSchema);
/**
* Generates a Targeting Schema, which just consists of a target field
* (ref to other model), number weight field, & optionally a
* children array. Used generically for a variety of targeting purposes
* in Campaign model.
*
* @param ref String name of referenced model
* @param [ref_type=Schema.ObjectId] Reference field type (default is ObjectId
* @param [children] optional field definition to pass to Schema.children,
* typically of a different instantiation of WeightTargetingSchema
* @returns {Schema}
*/
var WeightTargetingSchema = function(ref, ref_type, children){
ref_type = ref_type || Schema.ObjectId;
var schemaDefinition = {
target: { type: ref_type, ref: ref },
weight: { type: Number, max: 10, min: 0}
};
if (children) schemaDefinition.children = children;
return new Schema(schemaDefinition);
};
/**
* Schemas to handle store Inventory Settings param
*
* TODO: Nesting these is not going to be super-efficient when retrieving bid,
* TODO: especially if a lot of targets are set. Willing to overlook for now but should
* TODO: be revisited.
*/
var placementTargetSchema = WeightTargetingSchema('Placement', Schema.ObjectId);
var pageTargetSchema = WeightTargetingSchema('Page', Schema.ObjectId, [placementTargetSchema]);
var siteTargetSchema = WeightTargetingSchema('Site', Schema.ObjectId, [pageTargetSchema]);
var inventoryTargetSchema = WeightTargetingSchema('Clique', String, [siteTargetSchema]);
/**
* Generated a Block schema, which is similar to `WeightTargeting` schema except instead
* of `weight`, `explicit` field is used to indicate whether blocking at this level
* is to be enforced or ignored & assumed to be just an ancestor placeholder.
*/
var BlockSchema = function(ref, ref_type, children){
ref_type = ref_type || Schema.ObjectId;
var schemaDefinition = {
target: { type: ref_type, ref: ref },
//Currently, if explicit === true, then it will be blocked.
//Otherwise, it's assumed to be an ancestor placeholder and will be skipped.
//See statics.getInventoryBlockStatus for more details
explicit: { type: Boolean, default: false }
};
if (children) schemaDefinition.children = children;
return new Schema(schemaDefinition);
};
var placementBlockSchema = BlockSchema('Placement', Schema.ObjectId);
var pageBlockSchema = BlockSchema('Page', Schema.ObjectId, [placementBlockSchema]);
var siteBlockSchema = BlockSchema('Site', Schema.ObjectId, [pageBlockSchema]);
var inventoryBlockSchema = BlockSchema('Clique', String, [siteBlockSchema]);
/**
* Campaign schema holds most targeting logic
*
* @class
* @type {Schema}
*/
var campaignSchema = new Schema({
name: { type: String, required: true },
active: { type: Boolean, require: true, default: false },
description: String,
tstamp: { type: Date, default: Date.now },
budget: { type: Number, required: true },
start_date: { type: Date, required: true },
end_date: { type: Date },
even_pacing: { type: Boolean, default: true },
max_bid: { type: Number, required: true },
base_bid: { type: Number, required: true },
// TO BE DEPRECATED
placement_targets: [WeightTargetingSchema('Placement', Schema.ObjectId)],
dma_targets: [WeightTargetingSchema('DMA', Number)],
country_targets: [WeightTargetingSchema('Country', String)],
// TO BE DEPRECATED
blocked_cliques: [{ type: String, ref: 'Clique' }],
inventory_targets: [inventoryTargetSchema],
blocked_inventory: [inventoryBlockSchema],
frequency: { type: Number, default: 64 },
creativegroups: [creativeGroupSchema],
clique: { type: String, required: true, ref: 'Clique' }//TODO: Add validators to check string against Clique._id
});
/**
* Gets weighting for node based on inventory_targets setting based on
* parent-child inheritance w/ child override.
*
* !!NOTE!! This was written as a static method so that it could be easily
* serialized/deserialized to pass to BidAgent instances running in a primitive
* Node environment. As such, any modifications to this must not rely on any Node
* modules or libraries, only native ECMAScript functions & objects.
*
* @param branch array of ids representing branch, where top level = Clique & bottom = placement
* Ex: ['Outdoor', '9ruhu9302jnu9nnj','83hdnddknd678dua','83uhcbdbedndb24']
* @param inventory_targets any valid instance of inventoryTargetSchema
*/
campaignSchema.statics.getInventoryWeight = function(branch, inventory_targets){
if (!inventory_targets) return 1;
function _inner(branch, subtree, parentWeight){
var weight = parentWeight || 1;
var nodeId = branch.shift();
var targetObj = subtree.filter(function(elem){ return elem.target == nodeId; })[0];
if (targetObj){
if (targetObj.weight !== null){
// override parent weight w/ child
weight = targetObj.weight;
}
if (targetObj.children){
if (targetObj.children.length > 0){
weight = _inner(branch, targetObj.children, weight);
}
}
}
return weight;
}
return _inner(branch, inventory_targets);
};
/**
* Walks blocked_inventory tree setting to determine if branch is blocked.
*
* Currently, a branch is deemed to be "blocked" based on the 'explicit' setting of
* its nearest ancestor (including the node itself) stored in blocked_inventory.
*
* For example:
* ```
* $ var branch = ['Outdoor','123','456','789'];
* $ var blocked_inventory = [{
* target: Outdoor,
* explicit = false,
* children: [{
* target: '123',
* children: null //no other children are stored
* explicit: true //nearest ancestor in stored branch set to true
* }]
* }];
* $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
* true
*
* $ blocked_inventory = [{
* target: Outdoor,
* explicit = false,
* children: [{
* target: '123',
* explicit: true
* children: [{
* target: '456'
* explicit: false, //parent is set to false
* children: null //no other children are stored
* }]
* }]
* }];
* $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
* false
*
* $ blocked_inventory = [{
* target: Outdoor,
* explicit = false,
* children: [{
* target: '123',
* explicit: false,
* children: [{
* target: '456'
* explicit: false, //parent is set to false
* children: [{
* target: '789',
* explicit: true, // lowest level node in branch is stored w/ explicit = true
* children: null
* }]
* }]
* }]
* }];
* $ campaignSchema.getInventoryBlockStatus(branch, blocked_inventory);
* true
* ```
*
* **NOTE** This was written as a static method so that it could be easily
* serialized/deserialized to pass to BidAgent instances running in a primitive
* Node environment. As such, any modifications to this must not rely on any Node
* modules or libraries, only native ECMAScript functions & objects.
*
* @param {Array} branch array of ids representing branch, where top level = Clique & bottom = placement
* Ex: `['Outdoor', '9ruhu9302jnu9nnj','83hdnddknd678dua','83uhcbdbedndb24']`
* @param {inventoryBlockSchema} blocked_inventory any valid instance of inventoryBlockSchema
*/
campaignSchema.statics.getInventoryBlockStatus = function(branch, blocked_inventory){
if (!blocked_inventory) return false;
function _inner(branch, subtree, parentBlocked){
var isBlocked = parentBlocked || false;
var nodeId = branch.shift();
if (nodeId){
var targetObj = subtree.filter(function(elem){ return elem.target == nodeId; })[0];
if (targetObj){
// Set isBlocked based on `explicit` boolean param in block schema
// Parent objects
isBlocked = targetObj.explicit;
if (targetObj.children){
if (targetObj.children.length > 0){
isBlocked = _inner(branch, targetObj.children, isBlocked);
}
}
}
}
return isBlocked;
}
return _inner(branch, blocked_inventory);
};
campaignSchema.plugin(deepPopulate, {});
campaignSchema.plugin(mongooseApiQuery);
var Campaign = mongoose.model('Campaign', campaignSchema);
/**
* Action Beacon schema, representing beacons generated by advertisers to
* track downstream actions.
* @class
* @type {Schema}
*/
var actionBeaconSchema = new Schema({
name: { type: String, required: true},
click_lookback: { type: Number, default: 30 }, // this won't do anything for now, building in to future-proof
view_lookback: { type: Number, default: 15 }, // this won't do anything for now, building in to future-proof
match_view: { type: Boolean, default: true }, // this won't do anything for now, building in to future-proof
match_click: { type: Boolean, default: true } // this won't do anything for now, building in to future-proof
});
actionBeaconSchema.plugin(mongooseApiQuery);
var ActionBeacon = mongoose.model('ActionBeacon', actionBeaconSchema);
/**
* Advertiser schema, top-level container object.
*
* @class
* @type {Schema}
*/
var advertiserSchema = new Schema({
name: { type: String, required: true, index: true },
user: [{ type: Schema.ObjectId, ref: 'User' }],
organization: { type: Schema.ObjectId, ref: 'Organization' },
description: String,
logo_url: String,
nonprofit: { type: Boolean, require: true, default: false },
advertiser_fee: { type: Number, required: true, default: 0.10 },
website: { type: String, required: true },
tstamp: { type: Date, default: Date.now },
currency: { type: String, default: 'USD' },
campaigns: [campaignSchema],
actionbeacons: [actionBeaconSchema],
cliques: [{ type: String, required: true, ref: 'Clique' }] //TODO: Add validators to check string against Clique._id
});
// Virtual field to retrieve secure URL
advertiserSchema.virtual('logo_secure_url').get(function(){
if (this.logo_url){
return this.logo_url.replace('http://', 'https://');
}
});
advertiserSchema.set('toObject', { virtuals: true });
advertiserSchema.set('toJSON', { virtuals: true });
advertiserSchema.plugin(deepPopulate, {});
advertiserSchema.plugin(mongooseApiQuery);
advertiserSchema.set('autoIndex', false);
var Advertiser = mongoose.model('Advertiser', advertiserSchema);
/**
* TreeDocument subclass wrapping Advertiser document tree.
*
* @param {mongoose.connection} connection DB connection object
* @param {Object} options
* @constructor
*/
var AdvertiserModels = function(connection, options){
options = options || {};
this.connection = connection;
this.ActionBeacon = this.connection.model('ActionBeacon');
this.Advertiser = this.connection.model('Advertiser');
this.Campaign = this.connection.model('Campaign');
this.Creative = this.connection.model('Creative');
this.CreativeGroup = this.connection.model('CreativeGroup');
var branches = [['Campaign', 'CreativeGroup','Creative'], ['ActionBeacon']];
var top_level_node = 'Advertiser';
TreeDocument.call(this, top_level_node, branches, options);
};
util.inherits(AdvertiserModels, TreeDocument);
exports.AdvertiserModels = AdvertiserModels;