@@ -1,5 +1,5 @@
<div class="page-header">
<h1>Installation <small>Setup GUIA // Step 1</small></h1>
<h1><%- t('ns.plugin.setup:setup.user.title') %></h1>
</div>
<div class="row">
<div class="span12">
@@ -8,50 +8,50 @@ <h1>Installation <small>Setup GUIA // Step 1</small></h1>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="existingaccount" />
Use existing GUIA Social account
<input type="checkbox" id="existingaccount" disabled/>
<%- t('ns.plugin.setup:setup.user.form.existing') %>
</label>
</div>
</div>

<legend>User configuration</legend>
<legend><%- t('ns.plugin.setup:setup.user.form.user.legend') %></legend>
<div class="control-group">
<label class="control-label" for="xlInput">Username</label>
<label class="control-label" for="username"><%- t('ns.plugin.setup:setup.user.form.user.name') %></label>
<div class="controls">
<input type="text" size="30" id="username" class="input-xlarge" />
</div>
</div>

<div class="control-group">
<label class="control-label" for="xlInput">Password</label>
<label class="control-label" for="password"><%- t('ns.plugin.setup:setup.user.form.user.password') %></label>
<div class="controls">
<input type="password" size="30" id="password" class="input-xlarge" />
</div>
</div>

<div class="control-group">
<label class="control-label" for="xlInput">Re-Type Password</label>
<label class="control-label" for="repassword"><%- t('ns.plugin.setup:setup.user.form.user.repassword') %></label>
<div class="controls">
<input type="password" size="30" id="repassword" class="input-xlarge" />
</div>
</div>
</fieldset>

<fieldset>
<legend>GUIA Social details</legend>
<legend><%- t('ns.plugin.setup:setup.user.form.social.legend') %></legend>
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" id="transmit" checked="checked" />
Use GUIA Social as EPG source (recommended)
<input type="checkbox" id="transmit" disabled/>
<%- t('ns.plugin.setup:setup.user.form.social.use') %>
</label>
</div>
</div>

<div class="control-group">
<label class="control-label" for="xlInput">Email</label>
<label class="control-label" for="email"><%- t('ns.plugin.setup:setup.user.form.social.email') %></label>
<div class="controls">
<input type="text" size="30" id="email" class="input-xlarge" />
<span id="email" class="uneditable-input" />
</div>
</div>
</fieldset>
@@ -61,7 +61,7 @@ <h1>Installation <small>Setup GUIA // Step 1</small></h1>
<div class="row">
<div class="span12">
<div class="form-actions" style="text-align: right;">
<button class="btn" id="previous" next="Socialize">Back</button>&nbsp;<button class="btn btn-primary" id="next" next="StepTwo">Next step</button>
<button class="btn" id="previous" next="Socialize"><%- t('ns.plugin.setup:button.back') %></button>&nbsp;<button class="btn btn-primary" id="next" next="StepTwo"><%- t('ns.plugin.setup:button.next') %></button>
</div>
</div>
</div>
@@ -0,0 +1,277 @@
// Backbone.Validation v0.4.0
//
// Copyright (C)2011 Thomas Pedersen
// Distributed under MIT License
//
// Documentation and full license availabe at:
// http://github.com/thedersen/backbone.validation

Backbone.Validation = (function(Backbone, _, undefined) {
var defaultOptions = {
forceUpdate: false,
selector: 'name'
};

var getValidatedAttrs = function(model){
return _.reduce(_.keys(model.validation), function(memo, key){
memo[key] = undefined;
return memo;
}, {});
};

var getValidators = function(model, attr) {
var validation = model.validation[attr] || {};

if (_.isFunction(validation)) {
return validation;
} else if(_.isString(validation)) {
return model[validation];
} else if(!_.isArray(validation)) {
validation = [validation];
}

return _.reduce(validation, function(memo, validation){
_.each(_.without(_.keys(validation), 'msg'), function(validator){
memo.push({
fn: Backbone.Validation.validators[validator],
val: validation[validator],
msg: validation.msg
});
});
return memo;
}, []);
};

var validateAttr = function(model, attr, value) {
var validators = getValidators(model, attr);

if (_.isFunction(validators)) {
return validators.call(model, value, attr);
}

return _.reduce(validators, function(memo, validator){
var result = validator.fn(value, attr, validator.val, model);
if(result === false || memo === false) {
return false;
}
if (result && !memo) {
return validator.msg || result;
}
return memo;
}, '');
};

return {
version: '0.4.0',

configure: function(options) {
_.extend(defaultOptions, options);
},

bind: function(view, options) {
options = options || {};
var model = view.model,
forceUpdate = options.forceUpdate || defaultOptions.forceUpdate,
selector = options.selector || defaultOptions.selector,
validFn = options.valid || Backbone.Validation.callbacks.valid,
invalidFn = options.invalid || Backbone.Validation.callbacks.invalid,
isValid = _.isUndefined(model.validation) ? true : undefined;

model.validate = function(attrs) {
if(!attrs){
return model.validate.call(model, _.extend(getValidatedAttrs(model), model.toJSON()));
}

var result = [],
invalidAttrs = [];
isValid = true;

for (var changedAttr in attrs) {
var error = validateAttr(model, changedAttr, attrs[changedAttr]);
if (error) {
result.push(error);
invalidAttrs.push(changedAttr);
isValid = false;
invalidFn(view, changedAttr, error, selector);
} else {
validFn(view, changedAttr, selector);
}
}

if (isValid) {
for (var validatedAttr in model.validation) {
if (_.isUndefined(attrs[validatedAttr]) && validateAttr(model, validatedAttr, model.get(validatedAttr))) {
isValid = false;
break;
}
}
}

_.defer(function() {
model.trigger('validated', isValid, model, invalidAttrs);
model.trigger('validated:' + (isValid ? 'valid' : 'invalid'), model, invalidAttrs);
});

if(forceUpdate) {
return;
}

if (result.length === 1) {
return result[0];
}
if (result.length > 1) {
return result;
}
};

model.isValid = function(forceValidation) {
if(forceValidation) {
this.validate();
}
return isValid;
};
},

unbind: function(view) {
delete view.model.validate;
delete view.model.isValid;
}
};
} (Backbone, _));

Backbone.Validation.callbacks = {
valid: function(view, attr, selector) {
view.$('[' + selector + '~=' + attr + ']')
.removeClass('invalid')
.removeAttr('data-error');
},

invalid: function(view, attr, error, selector) {
view.$('[' + selector + '~=' + attr + ']')
.addClass('invalid')
.attr('data-error', error);
}
};

Backbone.Validation.patterns = {
digits: /^\d+$/,
number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i,
url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
};

Backbone.Validation.messages = {
required: '{0} is required',
acceptance: '{0} must be accepted',
min: '{0} must be grater than or equal to {1}',
max: '{0} must be less than or equal to {1}',
range: '{0} must be between {1} and {2}',
length: '{0} must be {1} characters',
minLength: '{0} must be at least {1} characters',
maxLength: '{0} must be at most {1} characters',
rangeLength: '{0} must be between {1} and {2} characters',
oneOf: '{0} must be one of: {1}',
equalTo: '{0} must be the same as {1}',
pattern: '{0} must be a valid {1}'
};

Backbone.Validation.validators = (function(patterns, messages, _) {
var trim = String.prototype.trim ?
function(text) {
return text === null ? '' : String.prototype.trim.call(text);
} :
function(text) {
var trimLeft = /^\s+/,
trimRight = /\s+$/;

return text === null ? '' : text.toString().replace(trimLeft, '').replace(trimRight, '');
};
var format = function() {
var args = Array.prototype.slice.call(arguments);
var text = args.shift();
return text.replace(/\{(\d+)\}/g, function(match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
};
var isNumber = function(value){
return _.isNumber(value) || (_.isString(value) && value.match(patterns.number));
};
var hasValue = function(value) {
return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === ''));
};

return {
fn: function(value, attr, fn, model) {
if(_.isString(fn)){
fn = model[fn];
}
return fn.call(model, value, attr);
},
required: function(value, attr, required, model) {
var isRequired = _.isFunction(required) ? required.call(model) : required;
if(!isRequired && !hasValue(value)) {
return false; // overrides all other validators
}
if (isRequired && !hasValue(value)) {
return format(messages.required, attr);
}
},
acceptance: function(value, attr) {
if(value !== 'true' && (!_.isBoolean(value) || value === false)) {
return format(messages.acceptance, attr);
}
},
min: function(value, attr, minValue) {
if (!isNumber(value) || value < minValue) {
return format(messages.min, attr, minValue);
}
},
max: function(value, attr, maxValue) {
if (!isNumber(value) || value > maxValue) {
return format(messages.max, attr, maxValue);
}
},
range: function(value, attr, range) {
if(!isNumber(value) || value < range[0] || value > range[1]) {
return format(messages.range, attr, range[0], range[1]);
}
},
length: function(value, attr, length) {
if (!hasValue(value) || trim(value).length !== length) {
return format(messages.length, attr, length);
}
},
minLength: function(value, attr, minLength) {
if (!hasValue(value) || trim(value).length < minLength) {
return format(messages.minLength, attr, minLength);
}
},
maxLength: function(value, attr, maxLength) {
if (!hasValue(value) || trim(value).length > maxLength) {
return format(messages.maxLength, attr, maxLength);
}
},
rangeLength: function(value, attr, range) {
if(!hasValue(value) || trim(value).length < range[0] || trim(value).length > range[1]) {
return format(messages.rangeLength, attr, range[0], range[1]);
}
},
oneOf: function(value, attr, values) {
if(!_.include(values, value)){
return format(messages.oneOf, attr, values.join(', '));
}
},
equalTo: function(value, attr, equalTo, model) {
console.log(model);

if(value !== model.get(equalTo)) {
return format(messages.equalTo, attr, equalTo);
}
},
pattern: function(value, attr, pattern) {
if (!hasValue(value) || !value.toString().match(patterns[pattern] || pattern)) {
return format(messages.pattern, attr, pattern);
}
}
};
} (Backbone.Validation.patterns, Backbone.Validation.messages, _));
@@ -1,46 +1,59 @@
var rest = require('restler');
var ChannelSchema = mongoose.model('Channel');
var async = require('async');
var mongoose = require('mongoose'),
rest = require('restler'),
ChannelSchema = mongoose.model('Channel'),
Configuration = mongoose.model('Configuration'),
async = require('async');

function ChannelImport (restful) {
this.restful = (vdr.restful === undefined || vdr.restful == "" || vdr.restful == null) ? restful : vdr.restful;
function ChannelImport () {
}

ChannelImport.prototype.start = function (callback) {
var self = this;
log.dbg("Starting channel import ... " + this.restful + '/channels.json?start=0');
ChannelImport.prototype.start = function (cb) {
var _this = this;

Configuration.findOne({}, function (err, doc) {
if (doc) {
_this.restful = 'http://' + doc.get('vdrHost') + ':' + doc.get('restfulPort');
_this.fetch(cb);
}
});

//log.dbg("Starting channel import ... " + this.restful + '/channels.json?start=0');
};

ChannelImport.prototype.fetch = function (cb) {
var _this = this;

rest.get(this.restful + '/channels.json?start=0').on('success', function(data) {
log.dbg("Fetched from VDR ...");
async.mapSeries(data.channels, function (channel, callback) {
//log.dbg("Fetched from VDR ...");

async.mapSeries(data.channels, function (channel, cb) {
if (global.socialize !== undefined && global.socialize && global.dnodeVdr) {
log.dbg('Sync channel: ' + channel.name);
//log.dbg('Sync channel: ' + channel.name);

dnodeVdr.getChannel(channel, function (res) {
res.number = channel.number;
res.image = channel.image;
res.group = channel.group;
self.save(res, function () {
log.dbg('Finished syncing: ' + channel.name);
callback(null);

_this.save(res, function () {
//log.dbg('Finished syncing: ' + channel.name);
cb(null);
});
});
} else {
log.dbg('Save channel: ' + channel.name);
self.save(channel, function () {
callback(null);
//log.dbg('Save channel: ' + channel.name);
_this.save(channel, function () {
cb(null);
});
}
}, function (err, result) {
callback();
cb();
});
});
};

ChannelImport.prototype.save = function (obj, callback) {
log.dbg('Save channel: ' + obj.name);
//log.dbg('Save channel: ' + obj.name);

var channelSchema = new ChannelSchema(obj);
channelSchema.save(function (err) {
@@ -1,8 +1,11 @@
var rest = require('restler');
var EventSchema = mongoose.model('Event');
var ActorSchema = mongoose.model('Actor');
var ChannelSchema = mongoose.model('Channel');
var async = require('async');
var rest = require('restler'),
mongoose = require('mongoose'),
log = require('node-logging'),
Configuration = mongoose.model('Configuration'),
EventSchema = mongoose.model('Event'),
ActorSchema = mongoose.model('Actor'),
ChannelSchema = mongoose.model('Channel'),
async = require('async');

Object.defineProperty(Object.prototype, "extend", {
enumerable: false,
@@ -19,18 +22,14 @@ Object.defineProperty(Object.prototype, "extend", {
}
});

function EpgImport (restful, numEvents) {
function EpgImport (numEvents) {
this.newEpg = false;

this.restful = restful;
this.numEvents = numEvents || 100;

var ChannelImport = require(__dirname + '/../Channel/Import');
this.channelImporter = new ChannelImport(restful);
}

EpgImport.prototype.start = function (callback) {
var self = this;
var _this = this;
this.newEpg = false;

log.dbg("Starting epg import ...");
@@ -39,23 +38,28 @@ EpgImport.prototype.start = function (callback) {

channelQuery.each(function (err, channel, next) {
if (channel === undefined) {
callback(self.newEpg);
callback(_this.newEpg);
return;
}

if (next === undefined) {
callback(self.newEpg);
callback(_this.newEpg);
return;
}

self.fetchEpg(channel, next);
Configuration.findOne({}, function (err, doc) {
if (doc) {
_this.restful = 'http://' + doc.get('vdrHost') + ':' + doc.get('restfulPort');
_this.fetchEpg(channel, next);
}
});
});
};

EpgImport.prototype.fetchEpg = function (channel, next) {
var self = this;

var from = new Date().getTime() / 1000 - (3600 * 24);
var from = parseInt(new Date().getTime() / 1000 - (3600 * 24));
var query = EventSchema.findOne({});

query.where('channel_id', channel._id);
@@ -82,7 +86,7 @@ EpgImport.prototype.fetchEpg = function (channel, next) {
log.dbg('Found ' + res.events.length + ' new events');

async.mapSeries(res.events, function (event, callback) {
if (socialize !== undefined && socialize && dnodeVdr) {
/*if (socialize !== undefined && socialize && dnodeVdr) {
log.dbg('Sync event ' + event.title);
var transmit = {
@@ -113,11 +117,11 @@ EpgImport.prototype.fetchEpg = function (channel, next) {
}
});
});
} else {
} else {*/
self.extractDetails(channel, event, function (event) {
self.insertEpg(event, callback);
});
}
//}
}, function (err, result) {
next.call();
});
@@ -1,39 +1,56 @@
var rest = require('restler');
var events = mongoose.model('Event');
var rest = require('restler'),
log = require('node-logging'),
mongoose = require('mongoose'),
Configuration = mongoose.model('Configuration'),
events = mongoose.model('Event');

function EpgTimer (restful) {
this.restful = restful;
}
function EpgTimer () {}

EpgTimer.prototype.getRestful = function (cb) {
var _this = this;

Configuration.findOne({}, function (err, doc) {
if (doc) {
_this.restful = 'http://' + doc.get('vdrHost') + ':' + doc.get('restfulPort');
cb.apply(_this, []);
}
});
};

EpgTimer.prototype.refresh = function () {
rest.get(this.restful + '/timers.json').on('success', function (res) {
res.timers.forEach(function (timer) {
var query = events.find({event_id: timer.event_id});
query.populate('channel_id', null, {channel_id: timer.channel});

query.each(function (err, doc, next) {
if (doc == null) {
return;
}

if (err || doc.channel_id == null) {
this.getRestful(function () {
rest.get(this.restful + '/timers.json').on('success', function (res) {
res.timers.forEach(function (timer) {
var query = events.find({event_id: timer.event_id});
query.populate('channel_id', null, {channel_id: timer.channel});

query.each(function (err, doc, next) {
if (doc == null) {
return;
}

if (err || doc.channel_id == null) {
next();
return;
}

doc.set({
timer_id: timer.id,
timer_active: timer.is_active,
timer_exists: timer.is_active
});

doc.save();

next();
return;
}

doc.set({
timer_id: timer.id,
timer_active: timer.is_active,
timer_exists: timer.is_active
});

doc.save();

next();
});

log.inf('Timer refresh finished');
}).on('error', function () {
log.err('Timer refresh failed');
return;
});
}).on('error', function () {
return;
});
};

@@ -174,10 +174,6 @@ Epg.prototype._buildEvent = function (doc, withSubEvents, callback) {
callback();
}
}, function (callback) {
/*dnode.getRating('X-Men', function (result) {
console.log(result);
callback();
});*/
callback();
}
], function () {
@@ -11,7 +11,7 @@

"day": {
"Monday": "Montag",
"Thusday": "Dienstag",
"Tuesday": "Dienstag",
"Wednesday": "Mittwoch",
"Thursday": "Donnerstag",
"Friday": "Freitag",
@@ -1,4 +1,15 @@
{
"Highlights <small>Today</small>": "Highlights <small>Today</small>",
"ns": {
"plugin": {
"tvguide": {
"header": {
"name": "ns.plugin.tvguide.header.name",
"title": "ns.plugin.tvguide.header.title"
}
}
}
},
"Morning program - 5": {
"00 to 12": {
"00 o'clock": "Morning program - 5.00 to 12.00 o'clock"
@@ -24,21 +35,6 @@
"00 o'clock": "Night program - 0.00 to 5.00 o'clock"
}
},
"yaVDR": "yaVDR",
"Ich": "Ich",
"Aufnahmen": "Aufnahmen",
"Kanäle": "Kanäle",
"TV Programm": "TV Programm",
"Highlights": "Highlights",
"day": {
"Saturday": "day.Saturday",
"Sunday": "day.Sunday",
"Tuesday": "day.Tuesday"
},
"app": {
"name": "app.name"
},
"Highlights <small>Today</small>": "Highlights <small>Today</small>",
"modal": {
"server": {
"disconnected": {
@@ -47,17 +43,13 @@
}
}
},
"Startseite": "Startseite",
"Information": "Information",
"Anmelden": "Anmelden",
"ns": {
"plugin": {
"tvguide": {
"header": {
"name": "ns.plugin.tvguide.header.name",
"title": "ns.plugin.tvguide.header.title"
}
}
}
"Highlights": "Highlights",
"TV Programm": "TV Programm",
"Kanäle": "Kanäle",
"Aufnahmen": "Aufnahmen",
"Ich": "Ich",
"yaVDR": "yaVDR",
"day": {
"Tuesday": "day.Tuesday"
}
}
@@ -31,5 +31,24 @@
"button": {
"next": "ns.plugin.setup:button.next",
"back": "ns.plugin.setup:button.back"
},
"setup": {
"user": {
"title": "ns.plugin.setup:setup.user.title",
"form": {
"existing": "ns.plugin.setup:setup.user.form.existing",
"user": {
"legend": "ns.plugin.setup:setup.user.form.user.legend",
"name": "ns.plugin.setup:setup.user.form.user.name",
"password": "ns.plugin.setup:setup.user.form.user.password",
"repassword": "ns.plugin.setup:setup.user.form.user.repassword"
},
"social": {
"legend": "ns.plugin.setup:setup.user.form.social.legend",
"use": "ns.plugin.setup:setup.user.form.social.use",
"email": "ns.plugin.setup:setup.user.form.social.email"
}
}
}
}
}
@@ -0,0 +1,277 @@
// Backbone.Validation v0.4.0
//
// Copyright (C)2011 Thomas Pedersen
// Distributed under MIT License
//
// Documentation and full license availabe at:
// http://github.com/thedersen/backbone.validation

Backbone.Validation = (function(Backbone, _, undefined) {
var defaultOptions = {
forceUpdate: false,
selector: 'name'
};

var getValidatedAttrs = function(model){
return _.reduce(_.keys(model.validation), function(memo, key){
memo[key] = undefined;
return memo;
}, {});
};

var getValidators = function(model, attr) {
var validation = model.validation[attr] || {};

if (_.isFunction(validation)) {
return validation;
} else if(_.isString(validation)) {
return model[validation];
} else if(!_.isArray(validation)) {
validation = [validation];
}

return _.reduce(validation, function(memo, validation){
_.each(_.without(_.keys(validation), 'msg'), function(validator){
memo.push({
fn: Backbone.Validation.validators[validator],
val: validation[validator],
msg: validation.msg
});
});
return memo;
}, []);
};

var validateAttr = function(model, attr, value) {
var validators = getValidators(model, attr);

if (_.isFunction(validators)) {
return validators.call(model, value, attr);
}

return _.reduce(validators, function(memo, validator){
var result = validator.fn(value, attr, validator.val, model);
if(result === false || memo === false) {
return false;
}
if (result && !memo) {
return validator.msg || result;
}
return memo;
}, '');
};

return {
version: '0.4.0',

configure: function(options) {
_.extend(defaultOptions, options);
},

bind: function(view, options) {
options = options || {};
var model = view.model,
forceUpdate = options.forceUpdate || defaultOptions.forceUpdate,
selector = options.selector || defaultOptions.selector,
validFn = options.valid || Backbone.Validation.callbacks.valid,
invalidFn = options.invalid || Backbone.Validation.callbacks.invalid,
isValid = _.isUndefined(model.validation) ? true : undefined;

model.validate = function(attrs) {
if(!attrs){
return model.validate.call(model, _.extend(getValidatedAttrs(model), model.toJSON()));
}

var result = [],
invalidAttrs = [];
isValid = true;

for (var changedAttr in attrs) {
var error = validateAttr(model, changedAttr, attrs[changedAttr]);
if (error) {
result.push(error);
invalidAttrs.push(changedAttr);
isValid = false;
invalidFn(view, changedAttr, error, selector);
} else {
validFn(view, changedAttr, selector);
}
}

if (isValid) {
for (var validatedAttr in model.validation) {
if (_.isUndefined(attrs[validatedAttr]) && validateAttr(model, validatedAttr, model.get(validatedAttr))) {
isValid = false;
break;
}
}
}

_.defer(function() {
model.trigger('validated', isValid, model, invalidAttrs);
model.trigger('validated:' + (isValid ? 'valid' : 'invalid'), model, invalidAttrs);
});

if(forceUpdate) {
return;
}

if (result.length === 1) {
return result[0];
}
if (result.length > 1) {
return result;
}
};

model.isValid = function(forceValidation) {
if(forceValidation) {
this.validate();
}
return isValid;
};
},

unbind: function(view) {
delete view.model.validate;
delete view.model.isValid;
}
};
} (Backbone, _));

Backbone.Validation.callbacks = {
valid: function(view, attr, selector) {
view.$('[' + selector + '~=' + attr + ']')
.removeClass('invalid')
.removeAttr('data-error');
},

invalid: function(view, attr, error, selector) {
view.$('[' + selector + '~=' + attr + ']')
.addClass('invalid')
.attr('data-error', error);
}
};

Backbone.Validation.patterns = {
digits: /^\d+$/,
number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,
email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i,
url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
};

Backbone.Validation.messages = {
required: '{0} is required',
acceptance: '{0} must be accepted',
min: '{0} must be grater than or equal to {1}',
max: '{0} must be less than or equal to {1}',
range: '{0} must be between {1} and {2}',
length: '{0} must be {1} characters',
minLength: '{0} must be at least {1} characters',
maxLength: '{0} must be at most {1} characters',
rangeLength: '{0} must be between {1} and {2} characters',
oneOf: '{0} must be one of: {1}',
equalTo: '{0} must be the same as {1}',
pattern: '{0} must be a valid {1}'
};

Backbone.Validation.validators = (function(patterns, messages, _) {
var trim = String.prototype.trim ?
function(text) {
return text === null ? '' : String.prototype.trim.call(text);
} :
function(text) {
var trimLeft = /^\s+/,
trimRight = /\s+$/;

return text === null ? '' : text.toString().replace(trimLeft, '').replace(trimRight, '');
};
var format = function() {
var args = Array.prototype.slice.call(arguments);
var text = args.shift();
return text.replace(/\{(\d+)\}/g, function(match, number) {
return typeof args[number] != 'undefined' ? args[number] : match;
});
};
var isNumber = function(value){
return _.isNumber(value) || (_.isString(value) && value.match(patterns.number));
};
var hasValue = function(value) {
return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === ''));
};

return {
fn: function(value, attr, fn, model) {
if(_.isString(fn)){
fn = model[fn];
}
return fn.call(model, value, attr);
},
required: function(value, attr, required, model) {
var isRequired = _.isFunction(required) ? required.call(model) : required;
if(!isRequired && !hasValue(value)) {
return false; // overrides all other validators
}
if (isRequired && !hasValue(value)) {
return format(messages.required, attr);
}
},
acceptance: function(value, attr) {
if(value !== 'true' && (!_.isBoolean(value) || value === false)) {
return format(messages.acceptance, attr);
}
},
min: function(value, attr, minValue) {
if (!isNumber(value) || value < minValue) {
return format(messages.min, attr, minValue);
}
},
max: function(value, attr, maxValue) {
if (!isNumber(value) || value > maxValue) {
return format(messages.max, attr, maxValue);
}
},
range: function(value, attr, range) {
if(!isNumber(value) || value < range[0] || value > range[1]) {
return format(messages.range, attr, range[0], range[1]);
}
},
length: function(value, attr, length) {
if (!hasValue(value) || trim(value).length !== length) {
return format(messages.length, attr, length);
}
},
minLength: function(value, attr, minLength) {
if (!hasValue(value) || trim(value).length < minLength) {
return format(messages.minLength, attr, minLength);
}
},
maxLength: function(value, attr, maxLength) {
if (!hasValue(value) || trim(value).length > maxLength) {
return format(messages.maxLength, attr, maxLength);
}
},
rangeLength: function(value, attr, range) {
if(!hasValue(value) || trim(value).length < range[0] || trim(value).length > range[1]) {
return format(messages.rangeLength, attr, range[0], range[1]);
}
},
oneOf: function(value, attr, values) {
if(!_.include(values, value)){
return format(messages.oneOf, attr, values.join(', '));
}
},
equalTo: function(value, attr, equalTo, model) {
console.log(model);

if(value !== model.get(equalTo)) {
return format(messages.equalTo, attr, equalTo);
}
},
pattern: function(value, attr, pattern) {
if (!hasValue(value) || !value.toString().match(patterns[pattern] || pattern)) {
return format(messages.pattern, attr, pattern);
}
}
};
} (Backbone.Validation.patterns, Backbone.Validation.messages, _));