mvcoffee-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ (function(){var DEFAULT_OPTS,MVCoffee,bind=function(fn,me){return function(){return fn.apply(me,arguments)}},slice=[].slice,hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;i<l;i++){if(i in this&&this[i]===item)return i}return-1};if(typeof exports!=="undefined"&&exports!==null){MVCoffee=exports}else{this.MVCoffee||(this.MVCoffee={});MVCoffee=this.MVCoffee}if(!Array.isArray){Array.isArray=function(arg){return Object.prototype.toString.call(arg)==="[object Array]"}}MVCoffee.Pluralizer=function(){function Pluralizer(){}Pluralizer.irregulars={man:"men",woman:"women",child:"children",person:"people",mouse:"mice",goose:"geese",datum:"data",alumnus:"alumni",hippopotamus:"hippopotami"};Pluralizer.addIrregulars=function(words){var plur,results,sing;results=[];for(sing in words){plur=words[sing];results.push(this.irregulars[sing]=plur)}return results};Pluralizer.addIrregular=Pluralizer.addIrregulars;Pluralizer.pluralize=function(word){var lastIndex,lastLetter,lastTwo,lastWord,len,result,words;words=word.split("_");lastIndex=words.length-1;lastWord=words[lastIndex];result=this.irregulars[lastWord];if(result){words[lastIndex]=result;return words.join("_")}len=lastWord.length;lastLetter=lastWord[len-1];lastTwo=lastWord.slice(len-2,+(len-1)+1||9e9);if(lastLetter==="s"||lastLetter==="z"){lastWord+="es"}else if(lastTwo==="ch"||lastTwo==="sh"){lastWord+="es"}else if(lastLetter==="y"){lastWord=lastWord.substring(0,len-1)+"ies"}else{lastWord+="s"}words[lastIndex]=lastWord;return words.join("_")};return Pluralizer}();DEFAULT_OPTS={debug:false,clientizeScope:"body"};MVCoffee.Runtime=function(){function Runtime(opts){var opt,value;if(opts==null){opts={}}this._ajaxWithClientize=bind(this._ajaxWithClientize,this);this.patch=bind(this.patch,this);this["delete"]=bind(this["delete"],this);this.post=bind(this.post,this);this.submit=bind(this.submit,this);this.fetch=bind(this.fetch,this);this.redirect=bind(this.redirect,this);this.visit=bind(this.visit,this);this.dontClientize=bind(this.dontClientize,this);this.clientize=bind(this.clientize,this);this.log=bind(this.log,this);this.processServerData=bind(this.processServerData,this);this._preProcessServerData=bind(this._preProcessServerData,this);this.getErrors=bind(this.getErrors,this);this.getSession=bind(this.getSession,this);this.setSession=bind(this.setSession,this);this.getFlash=bind(this.getFlash,this);this.setFlash=bind(this.setFlash,this);this.broadcast=bind(this.broadcast,this);this.narrowcast=bind(this.narrowcast,this);this.opts=DEFAULT_OPTS;for(opt in opts){value=opts[opt];this.opts[opt]=value}this.controllers={};this.modelStore=new MVCoffee.ModelStore;this.active=[];this.listeners=[];this._flash={};this._oldFlash={};this._clientizeCustomizations=[];this._neverClientizeSelectors=[];this.session={};this.dataId="mvcoffee_json";this.onfocusId=null}Runtime.prototype.register_controllers=function(contrs){var contr,id,results;results=[];for(id in contrs){contr=contrs[id];results.push(this._addController(new contr(id,this),id))}return results};Runtime.prototype._addController=function(contr,id){if(id!=null){return this.controllers[id]=contr}else{return this.controllers[contr.selector]=contr}};Runtime.prototype.register_listeners=function(){var args,j,len1,listenerClass,results;args=1<=arguments.length?slice.call(arguments,0):[];results=[];for(j=0,len1=args.length;j<len1;j++){listenerClass=args[j];results.push(this.listeners.push(new listenerClass(this)))}return results};Runtime.prototype.register_models=function(models){return this.modelStore.register_models(models)};Runtime.prototype.narrowcast=function(){var args,controller,i,message,messages,results,sent;controller=arguments[0],messages=arguments[1],args=3<=arguments.length?slice.call(arguments,2):[];if(!Array.isArray(messages)){messages=[messages]}sent=false;i=0;results=[];while(!sent&&i<messages.length){message=messages[i];if(message&&controller[message]!=null&&typeof controller[message]==="function"){sent=true;controller[message].apply(controller,args)}results.push(i++)}return results};Runtime.prototype.broadcast=function(){var args,controller,j,k,len1,len2,listener,messages,ref,ref1,results;messages=arguments[0],args=2<=arguments.length?slice.call(arguments,1):[];if(!Array.isArray(messages)){messages=[messages]}ref=this.active;for(j=0,len1=ref.length;j<len1;j++){controller=ref[j];this.narrowcast.apply(this,[controller,messages].concat(slice.call(args)))}ref1=this.listeners;results=[];for(k=0,len2=ref1.length;k<len2;k++){listener=ref1[k];results.push(this.narrowcast.apply(this,[listener,messages].concat(slice.call(args))))}return results};Runtime.prototype._recycleFlash=function(){this._oldFlash=this._flash;return this._flash={}};Runtime.prototype.setFlash=function(opts){var key,opt,results;results=[];for(key in opts){opt=opts[key];results.push(this._flash[key]=opt)}return results};Runtime.prototype.getFlash=function(key){var ref;return(ref=this._flash[key])!=null?ref:this._oldFlash[key]};Runtime.prototype.setSession=function(opts){var key,opt,results;results=[];for(key in opts){opt=opts[key];results.push(this.session[key]=opt)}return results};Runtime.prototype.getSession=function(key){return this.session[key]};Runtime.prototype.getErrors=function(){return this.errors};Runtime.prototype._preProcessServerData=function(data){var key,ref,value;if(data){if(this.opts.debug){this.log("Got data from server: "+JSON.stringify(data))}this.modelStore.load(data);if(data.flash!=null){this.setFlash(data.flash)}if(data.session!=null){ref=data.session;for(key in ref){value=ref[key];this.log("Setting session value "+key+" to value "+value+" from server");this.session[key]=value}}this.errors=data.errors;if(data.redirect!=null){this.redirect(data.redirect);return false}else{return true}}else{return true}};Runtime.prototype.processServerData=function(data,callback_message){var error_callback_message;if(callback_message==null){callback_message=""}if(this._preProcessServerData(data)){if(this.errors){if(callback_message){error_callback_message=[callback_message+"_errors","errors"]}else{error_callback_message="errors"}end;return this.broadcast(error_callback_message,this.errors)}else{return this.broadcast([callback_message,"render"])}}};Runtime.prototype.log=function(message){if(this.opts.debug){return console.log(message)}};Runtime.prototype.go=function(){var contr,id,json,newActive,parsed,ref,token;this.log("MVCoffee runtime firing 'go'");token=jQuery("meta[name='csrf-token']");if(token!=null?token.length:void 0){this.authenticity_token=token.attr("content")}this._recycleFlash();this.resetClientizeCustomizations();json=jQuery("#"+this.dataId).html();parsed=null;if(json){parsed=jQuery.parseJSON(json)}if(this._preProcessServerData(parsed)){newActive=[];ref=this.controllers;for(id in ref){contr=ref[id];if(jQuery("#"+id).length>0){this.log("Starting controller identified by "+id);newActive.push(contr)}}if(this.active.length){this.broadcast("stop");window.onbeforeunload=null;window.onfocus=null;window.onblur=null;if(this.onfocusId){clearInterval(this.onfocusId)}this.onfocusId=null}if(newActive.length){this.active=newActive;this.broadcast("start");window.onfocus=function(_this){return function(){_this._startSafariKludge();return _this.broadcast("resume")}}(this);window.onblur=function(_this){return function(){_this._stopSafariKludge();return _this.broadcast("pause")}}(this);this._startSafariKludge()}else{this.active=[]}this.clientize();return this.broadcast("render")}};Runtime.prototype._startSafariKludge=function(){this._stopSafariKludge();this.lastFired=(new Date).getTime();return this.onfocusId=setInterval(function(_this){return function(){var now;now=(new Date).getTime();if(now-_this.lastFired>2e3){_this.broadcast("pause");_this.broadcast("resume")}return _this.lastFired=now}}(this),500)};Runtime.prototype._stopSafariKludge=function(){if(this.onfocusId!=null){clearInterval(this.onfocusId)}return this.onfocusId=null};Runtime.prototype.clientize=function(scope){var $searchInside,applyClientize,self;if(scope==null){scope=null}if(typeof Turbolinks!=="undefined"&&Turbolinks!==null){self=this;scope=scope!=null?scope:this.opts.clientizeScope;$searchInside=jQuery(scope);applyClientize=function(selector,event,validation,submission){return $searchInside.find(selector).each(function(index,element){var customization,j,len1,ref,thisCustom;customization={};ref=self._clientizeCustomizations;for(j=0,len1=ref.length;j<len1;j++){thisCustom=ref[j];if(jQuery(element).is(thisCustom.selector)){customization=thisCustom}}if(!customization.ignore){return jQuery(element).on(event,function(eventObject){var callback,confirm,doPost;callback=element.id;if(customization.callback){callback=customization.callback}doPost=validation(customization,callback);if(doPost){confirm=jQuery(element).data("confirm");if(customization.confirm){confirm=customization.confirm}if(confirm){if(confirm instanceof Function){doPost=confirm()}else{doPost=window.confirm(confirm)}}}if(doPost){submission(element,callback)}return false})}})};applyClientize("form","submit",function(customization,callback){var method,model;model=customization.model;if(model!=null){model.populate();method="errors";if(callback){method=[callback+"_errors","errors"]}if(customization.controller!=null){self.narrowcast(customization.controller,method,model.errors)}return model.isValid()}else{return true}},function(element,callback){var params,url;if(element.method==="get"||element.method==="GET"){params=jQuery(element).serialize();url=element.action;if(params){if(/\?/.test(url)){url+="&"+params}else{url+="?"+params}}return self.visit(url)}else{return self.submit(element,callback)}});return applyClientize("a","click",function(customization,callback){return true},function(element,callback){var method;method=jQuery(element).data("method");if(method==="post"){return self.post(element.href,{},callback)}else if(method==="delete"){return self["delete"](element.href,{},callback)}else if(method==="patch"){return self.patch(element.href,{},callback)}else{return self.visit(element.href)}})}};Runtime.prototype.neverClientize=function(){var arg,args,j,len1,results;args=1<=arguments.length?slice.call(arguments,0):[];results=[];for(j=0,len1=args.length;j<len1;j++){arg=args[j];results.push(this._neverClientizeSelectors.push({selector:arg,ignore:true}))}return results};Runtime.prototype.resetClientizeCustomizations=function(){return this._clientizeCustomizations=this._neverClientizeSelectors.slice()};Runtime.prototype.addClientizeCustomization=function(customization){return this._clientizeCustomizations.push(customization)};Runtime.prototype.dontClientize=function(selector){return this._clientizeCustomizations.push({selector:selector,ignore:true})};Runtime.prototype._setSessionCookie=function(){var cookie,expiration,params;params=jQuery.param(this.session);expiration=new Date;expiration.setTime(expiration.getTime()+1e3);cookie="mvcoffee_session="+params+"; path=/; expires="+expiration.toGMTString();document.cookie=cookie;return this.log("Sending cookie = "+cookie)};Runtime.prototype.visit=function(url){this._recycleFlash();this._setSessionCookie();return Turbolinks.visit(url)};Runtime.prototype.redirect=function(url){this._setSessionCookie();return Turbolinks.visit(url)};Runtime.prototype.fetch=function(url,callback_message){if(callback_message==null){callback_message=""}this._setSessionCookie();return jQuery.get(url,null,function(_this){return function(data){return _this.processServerData(data,callback_message)}}(this),"json")};Runtime.prototype.submit=function(submitee,callback_message){var element;if(callback_message==null){callback_message=""}this._setSessionCookie();element=submitee;if(submitee instanceof jQuery){element=submitee.get(0)}jQuery.post(element.action,jQuery(element).serialize(),function(_this){return function(data){return _this.processServerData(data,callback_message)}}(this),"json");return false};Runtime.prototype.post=function(url,params,callback_message){if(params==null){params={}}if(callback_message==null){callback_message=""}return this._ajaxWithClientize("POST",url,params,callback_message)};Runtime.prototype["delete"]=function(url,params,callback_message){if(params==null){params={}}if(callback_message==null){callback_message=""}return this._ajaxWithClientize("DELETE",url,params,callback_message)};Runtime.prototype.patch=function(url,params,callback_message){if(params==null){params={}}if(callback_message==null){callback_message=""}return this._ajaxWithClientize("PATCH",url,params,callback_message)};Runtime.prototype._ajaxWithClientize=function(type,url,params,callback_message){var self;this._setSessionCookie();self=this;return jQuery.ajax({url:url,data:params,type:type,success:function(_this){return function(data){return self.processServerData(data,callback_message)}}(this),dataType:"json"})};Runtime.prototype.run=function(){var self;this.log("MVCoffee runtime run");self=this;jQuery(function(){return self.go()});return jQuery(document).on("pagebeforeshow",function(){return self.go()})};return Runtime}();MVCoffee.Controller=function(){function Controller(id1,_runtime){this.id=id1;this._runtime=_runtime;this.errors=bind(this.errors,this);this.selector="#"+this.id;this.timerId=null;this.isActive=false;this.processServerData=this._runtime.processServerData;this.getFlash=this._runtime.getFlash;this.setFlash=this._runtime.setFlash;this.getSession=this._runtime.getSession;this.setSession=this._runtime.setSession;this.getErrors=this._runtime.getErrors;this.broadcast=this._runtime.broadcast;this.dontClientize=this._runtime.dontClientize;this.reclientize=this._runtime.clientize;this.visit=this._runtime.visit;this.fetch=this._runtime.fetch;this.post=this._runtime.post;this["delete"]=this._runtime["delete"];this.patch=this._runtime.patch;this.submit=this._runtime.submit;this.log=this._runtime.log;this.timerCount=0}Controller.prototype.addClientizeCustomization=function(customization){customization.controller=this;return this._runtime.addClientizeCustomization(customization)};Controller.prototype.rerender=function(opts){var $element,as_var,collection,element,item,j,len1,locals,ref,ref1,template_path;element=(ref=opts.selector)!=null?ref:opts.element;$element=jQuery(element);template_path=opts.template;locals=(ref1=opts.locals)!=null?ref1:{};$element.empty();collection=opts.collection;if(collection){as_var=opts.as;if(as_var){for(j=0,len1=collection.length;j<len1;j++){item=collection[j];locals[as_var]=item;$element.append(JST[template_path](locals))}}}else{$element.append(JST[template_path](locals))}return this.reclientize($element)};Controller.prototype.start=function(){this.isActive=true;this.onStart();if(this.refresh!=null){return this.startTimer()}};Controller.prototype.resume=function(){this.onResume();if(this.refresh!=null&&!this.isActive){this.isActive=true;this.refresh();return this.startTimer()}};Controller.prototype.pause=function(){this.onPause();if(this.refresh!=null){this.isActive=false;return this.stopTimer()}};Controller.prototype.stop=function(){this.isActive=false;this.onStop();if(this.refresh!=null){return this.stopTimer()}};Controller.prototype.refreshInterval=6e4;Controller.prototype.refresh=null;Controller.prototype.onStart=function(){};Controller.prototype.onPause=function(){};Controller.prototype.onResume=function(){};Controller.prototype.onStop=function(){};Controller.prototype.render=function(){};Controller.prototype.errors=function(errors){return console.log("!!!!! The errors method was called on controller "+this.toString()+" but not implemented!!!!!")};Controller.prototype.toString=function(){return this.id};Controller.prototype.startTimer=function(){var self;if(this.timerId!=null){this.stopTimer()}if(this.refreshInterval!=null&&this.refreshInterval>0){self=this;this.timerCount+=1;return this.timerId=setInterval(function(){return self.refresh.call(self)},this.refreshInterval)}};Controller.prototype.stopTimer=function(){if(this.timerId!=null){clearInterval(this.timerId)}return this.timerId=null};return Controller}();MVCoffee.ModelStore=function(){ModelStore.prototype.MIN_DATA_FORMAT_VERSION="1.0.0";function ModelStore(models){if(models==null){models={}}this.modelDefs={};this.store={};this.register_models(models)}ModelStore.prototype.register_models=function(models){var classdef,name,results;if(models==null){models={}}results=[];for(name in models){classdef=models[name];results.push(this._addModel(name,classdef))}return results};ModelStore.prototype._addModel=function(name,classdef){var base,base1;this.modelDefs[name]=classdef;(base=classdef.prototype).modelName||(base.modelName=name);(base1=classdef.prototype).modelNamePlural||(base1.modelNamePlural=MVCoffee.Pluralizer.pluralize(name));classdef.prototype.modelStore=this;return this.store[name]={}};ModelStore.prototype.knowsAbout=function(name){return this.store[name]!=null};ModelStore.prototype.load_model_data=function(modelName,data){var j,len1,model,modelObj,results;if(Array.isArray(data)){results=[];for(j=0,len1=data.length;j<len1;j++){modelObj=data[j];model=new this.modelDefs[modelName](modelObj);results.push(this.store[modelName][model.id]=model)}return results}else{model=new this.modelDefs[modelName](data);return this.store[modelName][model.id]=model}};ModelStore.prototype.load=function(object){var commands,foreignKeys,j,k,len1,len2,modelId,modelName,record,ref,ref1,ref2,results,toBeRemoved;if(object.mvcoffee_version==null||object.mvcoffee_version<this.MIN_DATA_FORMAT_VERSION){throw"MVCoffee.DataStore requires minimum data format "+this.MIN_DATA_FORMAT_VERSION}ref=object.models;for(modelName in ref){commands=ref[modelName];if(this.modelDefs[modelName]!=null){if(commands.replace_on!=null){if(Array.isArray(commands.replace_on)){toBeRemoved=[];ref1=commands.replace_on;for(j=0,len1=ref1.length;j<len1;j++){foreignKeys=ref1[j];toBeRemoved=toBeRemoved.concat(this.where(modelName,foreignKeys))}}else{toBeRemoved=this.where(modelName,commands.replace_on)}for(k=0,len2=toBeRemoved.length;k<len2;k++){record=toBeRemoved[k];this.remove(modelName,record.id)}}}}ref2=object.models;results=[];for(modelName in ref2){commands=ref2[modelName];if(this.modelDefs[modelName]!=null){if(commands.data!=null){this.load_model_data(modelName,commands.data)}if(commands["delete"]!=null){if(Array.isArray(commands["delete"])){results.push(function(){var l,len3,ref3,results1;ref3=commands["delete"];results1=[];for(l=0,len3=ref3.length;l<len3;l++){modelId=ref3[l];results1.push(this._delete_with_cascade(modelName,modelId))}return results1}.call(this))}else{results.push(this._delete_with_cascade(modelName,commands["delete"]))}}else{results.push(void 0)}}else{results.push(void 0)}}return results};ModelStore.prototype.save=function(modelName,modelObj){var ref;return(ref=this.store[modelName])!=null?ref[modelObj.id]=modelObj:void 0};ModelStore.prototype.find=function(model,id){var ref;return(ref=this.store[model])!=null?ref[id]:void 0};ModelStore.prototype.findBy=function(model,conditions){var id,match,prop,record,records,ref,result,value;records=(ref=this.store[model])!=null?ref:{};result=null;for(id in records){record=records[id];match=true;for(prop in conditions){value=conditions[prop];if(record[prop]!==value){match=false}}if(match){result||(result=record)}}return result};ModelStore.prototype.where=function(model,conditions){var id,match,prop,record,records,result,value;records=this.store[model];result=[];for(id in records){record=records[id];match=true;for(prop in conditions){value=conditions[prop];if(record[prop]!==value){match=false}}if(match){result.push(record)}}return result};ModelStore.prototype.all=function(model){var id,record,records,result;records=this.store[model];result=[];for(id in records){record=records[id];result.push(record)}return result};ModelStore.prototype["delete"]=function(model,id){return delete this.store[model][id]};ModelStore.prototype.remove=function(model,id){return delete this.store[model][id]};ModelStore.prototype._delete_with_cascade=function(model,id){var record;record=this.store[model][id];if(record["delete"]!=null&&record["delete"]instanceof Function){return record["delete"]()}else{return delete this.store[model][id]}};return ModelStore}();MVCoffee.Model=function(){function Model(obj){this.addError=bind(this.addError,this);if(obj!=null){this.update(obj)}}Model.prototype.modelName=null;Model.prototype.fields=[];Model.prototype._associations_children=[];Model.prototype.errors=[];Model.prototype.errorsForField={};Model.prototype.valid=true;Model.order=function(array,order,opts){var desc,ignoreCase,prop,ref,result,value;if(opts==null){opts={}}result=array;ref=order.split(/\s+/),prop=ref[0],desc=ref[1];value=1;if(desc!=null&&desc==="desc"){value=-1}ignoreCase=false;if(opts.ignoreCase){ignoreCase=true}result.sort(function(a,b){a=a[prop];b=b[prop];if(ignoreCase){if(a.toLowerCase){a=a.toLowerCase()}if(b.toLowerCase){b=b.toLowerCase()}}if(a>b){return value}else if(a<b){return-value}else{return 0}});return result};Model.all=function(options){var result;if(options==null){options={}}result=this.prototype.modelStore.all(this.prototype.modelName);if(options.order){result=this.order(result,options.order,options)}return result};Model.find=function(id){return this.prototype.modelStore.find(this.prototype.modelName,id)};Model.findBy=function(conditions){return this.prototype.modelStore.findBy(this.prototype.modelName,conditions)};Model.where=function(conditions){return this.prototype.modelStore.where(this.prototype.modelName,conditions)};Model.prototype.save=function(){if(this.validate()){return this.modelStore.save(this.modelName,this)}};Model.prototype.store=function(){return this.modelStore.save(this.modelName,this)};Model.prototype.update=function(obj){var field,results,value;results=[];for(field in obj){if(!hasProp.call(obj,field))continue;value=obj[field];if((value instanceof Object||value instanceof Array)&&this.modelStore.knowsAbout(field)){results.push(this.modelStore.load_model_data(field,value))}else{results.push(this[field]=value)}}return results};Model.prototype["delete"]=function(){var assoc,child,children,j,k,len1,len2,ref;ref=this._associations_children;for(j=0,len1=ref.length;j<len1;j++){assoc=ref[j];children=this[assoc]();if(Array.isArray(children)){for(k=0,len2=children.length;k<len2;k++){child=children[k];child["delete"]()}}else{if(children!=null){children["delete"]()}}}return this.modelStore["delete"](this.modelName,this.id)};Model.prototype.destroy=function(){return this["delete"]()};Model.prototype.remove=function(){return this.modelStore.remove(this.modelName,this.id)};Model.findFieldIndex=function(field){var fields,i,index,j,ref;fields=this.prototype.fields;index=-1;for(i=j=0,ref=fields.length;0<=ref?j<ref:j>ref;i=0<=ref?++j:--j){if(fields[i].name===field){index=i}}return index};Model.validates=function(field,test){var fields,index;if(!this.prototype.hasOwnProperty("fields")){this.prototype.fields=[]}fields=this.prototype.fields;index=this.findFieldIndex(field);if(index<0){return fields.push({name:field,validates:[test]})}else{field=fields[index];if(field.validates!=null){if(Array.isArray(field.validates)){return field.validates.push(test)}else{return field.validates=[field.validates,test]}}else{return field.validates=[test]}}};Model.types=function(field,type){var fields,index;if(!this.prototype.hasOwnProperty("fields")){this.prototype.fields=[]}fields=this.prototype.fields;index=this.findFieldIndex(field);if(index<0){return fields.push({name:field,type:type})}else{return fields[index].type=type}};Model.displays=function(field,display){var fields,index;if(!this.prototype.hasOwnProperty("fields")){this.prototype.fields=[]}fields=this.prototype.fields;index=this.findFieldIndex(field);if(index<0){return fields.push({name:field,display:display})}else{return fields[index].display=display}};Model.hasMany=function(name,options){var methodName,self;if(options==null){options={}}methodName=options.as||MVCoffee.Pluralizer.pluralize(name);if(!this.prototype.hasOwnProperty("_associations_children")){this.prototype._associations_children=[]}this.prototype._associations_children.push(methodName);self=this;return this.prototype[methodName]=function(){var constraints,foreignKey,j,join,joinTable,joins,len1,modelStore,record,result;modelStore=self.prototype.modelStore;foreignKey=options.foreignKey||options.foreign_key||self.prototype.modelName+"_id";result=[];if(modelStore!=null){constraints={};constraints[foreignKey]=this.id;if(options.through){joinTable=options.through;joins=modelStore.where(joinTable,constraints);for(j=0,len1=joins.length;j<len1;j++){join=joins[j];record=modelStore.find(name,join[name+"_id"]);if(record){result.push(record)}}}else{result=modelStore.where(name,constraints)}}if(options.order){result=self.order(result,options.order)}return result}};Model.has_many=Model.hasMany;Model.hasOne=function(name,options){var methodName,self;if(options==null){options={}}methodName=options.as||name;if(!this.prototype.hasOwnProperty("_associations_children")){this.prototype._associations_children=[]}this.prototype._associations_children.push(methodName);self=this;return this.prototype[methodName]=function(){var constraints,foreignKey,modelStore,result;modelStore=self.prototype.modelStore;foreignKey=options.foreignKey||options.foreign_key||self.prototype.modelName+"_id";result=null;if(modelStore!=null){constraints={};constraints[foreignKey]=this.id;result=modelStore.findBy(name,constraints)}return result}};Model.has_one=Model.hasOne;Model.belongsTo=function(name,options){var foreignKey,methodName,self;if(options==null){options={}}methodName=options.as||name;foreignKey=options.foreignKey||options.foreign_key||name+"_id";self=this;return this.prototype[methodName]=function(){var modelStore,result;modelStore=self.prototype.modelStore;result=null;if(modelStore!=null){result=modelStore.find(name,this[foreignKey])}return result}};Model.belongs_to=Model.belongsTo;Model.prototype.isValid=function(){return this.valid};Model.prototype.populate=function(obj){var field,j,len1,ref,selector;if(obj!=null){this.update(obj)}else{ref=this.fields;for(j=0,len1=ref.length;j<len1;j++){field=ref[j];if(this.modelName!=null){selector="#"+this.modelName+"_"+field.name}else{selector="#"+field.name}if(field.type!=null&&field.type==="boolean"){this[field.name]=jQuery(selector).is(":checked")}else{this[field.name]=jQuery(selector).val()}}}return this.validate()};Model.prototype.validate=function(){var confirm,field,isNumber,j,k,len1,len2,matches,ref,subval,tokenizer,validation,validations,value;this.valid=true;this.errors=[];this.errorsForField={};ref=this.fields;for(j=0,len1=ref.length;j<len1;j++){field=ref[j];if(field.validates!=null){validations=field.validates;if(!Array.isArray(validations)){validations=[validations]}for(k=0,len2=validations.length;k<len2;k++){validation=validations[k];if(validation.test==="acceptance"){value=validation.accept;if(value==null){value="1"}this.__performValidation(field,validation,null,"must be accepted",function(val){return val===value})}else if(validation.test==="confirmation"){confirm=this[field.name+"_confirmation"];this.__performValidation(field,validation,null,"doesn't match confirmation",function(val){return val!=null&&val!==""&&confirm!=null&&val===confirm})}else if(validation.test==="email"){this.__performValidation(field,validation,null,"must be a valid email address",function(val){return val!=null&&val.match(/^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/)})}else if(validation.test==="exclusion"){matches=validation["in"]||[];this.__performValidation(field,validation,null,"is reserved",function(val){return val!=null&&!(indexOf.call(matches,val)>=0)})}else if(validation.test==="format"){matches=validation["with"]||/.*/;this.__performValidation(field,validation,null,"is invalid",function(val){return val!=null&&matches.test(val)})}else if(validation.test==="inclusion"){matches=validation["in"]||[];this.__performValidation(field,validation,null,"is not included in the list",function(val){return val!=null&&indexOf.call(matches,val)>=0})}else if(validation.test==="length"){tokenizer=function(val){return val.split("")};if(validation.tokenizer!=null){tokenizer=validation.tokenizer}if(validation.minimum!=null){if(typeof validation.minimum==="number"){subval=null;value=validation.minimum}else{subval=validation.minimum;value=subval.value}this.__performValidation(field,validation,subval,"is too short (minimum is "+value+" characters)",function(val){return val!=null&&tokenizer(val).length>=value})}if(validation.maximum!=null){if(typeof validation.maximum==="number"){subval=null;value=validation.maximum}else{subval=validation.maximum;value=subval.value}this.__performValidation(field,validation,subval,"is too long (maximum is "+value+" characters)",function(val){return val==null||tokenizer(val).length<=value})}if(validation["is"]!=null){if(typeof validation["is"]==="number"){subval=null;value=validation["is"]}else{subval=validation["is"];value=subval.value}this.__performValidation(field,validation,subval,"is the wrong length (must be "+value+" characters)",function(val){return val!=null&&tokenizer(val).length===value})}}else if(validation.test==="numericality"){if(validation.only_integer!=null&&validation.only_integer){isNumber=this.__performValidation(field,validation,null,"must be an integer",function(val){return/^[-+]?\d+$/.test(val)})}else{isNumber=this.__performValidation(field,validation,null,"must be a number",function(val){var number;number=parseFloat(val);return/^[-+]?\d*\.?\d*(e\d+)?$/.test(val)&&number===number})}if(isNumber){if(validation.greater_than!=null){if(typeof validation.greater_than==="number"){value=validation.greater_than;subval=null}else{subval=validation.greater_than;value=subval.value}this.__performValidation(field,validation,subval,"must be greater than "+value,function(val){return parseFloat(val)>value})}if(validation.greater_than_or_equal_to!=null){if(typeof validation.greater_than_or_equal_to==="number"){subval=null;value=validation.greater_than_or_equal_to}else{subval=validation.greater_than_or_equal_to;value=subval.value}this.__performValidation(field,validation,subval,"must be greater than or equal to "+value,function(val){return parseFloat(val)>=value})}if(validation.equal_to!=null){if(typeof validation.equal_to==="number"){subval=null;value=validation.equal_to}else{subval=validation.equal_to;value=subval.value}this.__performValidation(field,validation,subval,"must be equal to "+value,function(val){return parseFloat(val)===value})}if(validation.less_than!=null){if(typeof validation.less_than==="number"){value=validation.less_than;subval=null}else{subval=validation.less_than;value=subval.value}this.__performValidation(field,validation,subval,"must be less than "+value,function(val){return parseFloat(val)<value})}if(validation.less_than_or_equal_to!=null){if(typeof validation.less_than_or_equal_to==="number"){subval=null;value=validation.less_than_or_equal_to}else{subval=validation.less_than_or_equal_to;value=subval.value}this.__performValidation(field,validation,subval,"must be less than or equal to "+value,function(val){return parseFloat(val)<=value})}if(validation.odd!=null){if(typeof validation.odd==="boolean"){subval=null;value=validation.odd}else{subval=validation.odd;value=true}if(value){this.__performValidation(field,validation,subval,"must be odd",function(val){return Math.abs(parseFloat(val))%2===1})}}if(validation.even!=null){if(typeof validation.even==="boolean"){subval=null;value=validation.even}else{subval=validation.even;value=true}if(value){this.__performValidation(field,validation,subval,"must be even",function(val){return parseFloat(val)%2===0})}}}}else if(validation.test==="presence"){this.__performValidation(field,validation,null,"can't be empty",function(val){return val!=null&&/\S+/.test(val)})}else if(validation.test==="absence"){this.__performValidation(field,validation,null,"must be blank",function(val){return!(val!=null&&/\S+/.test(val))})}else{if(typeof this[validation.test]!=="function"){
2
+ throw"custom validation is not a function"}this.__performValidation(field,validation,null,"is invalid",this[validation.test])}}}}return this.valid};Model.prototype.__performValidation=function(field,validation,subval,message,comparison){var data;if(validation.only_if!=null&&!this[validation.only_if]()){return true}if(validation.unless!=null&&this[validation.unless]()){return true}if(subval!=null){if(subval.only_if!=null&&!this[subval.only_if]()){return true}if(subval.unless!=null&&this[subval.unless]()){return true}}data=this[field.name];if(validation.allow_null!=null&&validation.allow_null&&data==null){return true}if(validation.allow_blank!=null&&validation.allow_blank&&(data==null||!/\S+/.test(data))){return true}if(!comparison(data)){this.addError(field,validation,subval,message);return false}return true};Model.prototype.addError=function(field,validation,subval,message){var base,errorMessage,name,name1;this.valid=false;name=field.display;if(name==null){name=field.name;if(name.length>0){name=name[0].toUpperCase()+name.slice(1);name=name.split("_").join(" ")}}if((subval!=null?subval.message:void 0)!=null){errorMessage=name+" "+subval.message}else if((validation!=null?validation.message:void 0)!=null){errorMessage=name+" "+validation.message}else{errorMessage=name+" "+message}this.errors.push(errorMessage);if((base=this.errorsForField)[name1=field.name]==null){base[name1]=[]}return this.errorsForField[field.name].push(errorMessage)};return Model}()}).call(this);
@@ -0,0 +1,12 @@
1
+ module MvcoffeeHelper
2
+
3
+ def mvcoffee_json_tag(mvcoffee)
4
+ result = ' <script id="mvcoffee_json" type="text/json">'
5
+ result += raw mvcoffee.to_json
6
+ result += ' </script>'
7
+
8
+ result.html_safe
9
+ end
10
+
11
+
12
+ end
@@ -0,0 +1,446 @@
1
+ module MVCoffee
2
+ class MVCoffee
3
+ def initialize(client_session = {})
4
+ @json = {
5
+ mvcoffee_version: Mvcoffee::Rails::VERSION,
6
+ flash: {},
7
+ models: {},
8
+ session: {}
9
+ }
10
+
11
+ @client_session = client_session
12
+ end
13
+
14
+ # Instructs the client to perform a redirect to the path provided as the first
15
+ # argument. This is preferable to issuing a redirect on the server because
16
+ # 1. it is guaranteed to keep the client javascript session live (keeping the
17
+ # cache intact), and 2. it will perform redirects regardless of whether the
18
+ # incoming request was performed as a regular html request or an ajax request for
19
+ # json (whereas the server can't issue a redirect with the format json).
20
+ #
21
+ # The optional hash parameters are added to the client-side flash.
22
+ # For example:
23
+ # @mvcoffee.set_redirect some_path, notice: 'Everything is okey-dokey!'
24
+ # will set the client flash['notice'] to the silly message.
25
+ def set_redirect(path, opts = {})
26
+ set_flash opts
27
+ @json[:redirect] = path
28
+ end
29
+
30
+ def redirect
31
+ @json[:redirect]
32
+ end
33
+
34
+ def flash
35
+ @json[:flash]
36
+ end
37
+
38
+ # Set's the client-side flash. Takes a hash of keys and values, and merges them
39
+ # into the existing flash for this request.
40
+ #
41
+ # The flash on the client will cycle out after two requests. In other words, it
42
+ # will persist after one redirect, but will take on new values after the next
43
+ # request.
44
+ def set_flash(opts = {})
45
+ @json[:flash].merge! opts
46
+ if opts[:errors]
47
+ set_errors opts[:errors]
48
+ end
49
+ end
50
+
51
+
52
+ def set_session(opts)
53
+ @json[:session].merge! opts
54
+ end
55
+
56
+
57
+ def client_session(key)
58
+ value = @client_session[key]
59
+ unless value.nil?
60
+ if value.respond_to? :[]
61
+ value[0]
62
+ else
63
+ value
64
+ end
65
+ end
66
+ end
67
+
68
+ # Takes an array of errors and sends them to the client. Usually this should be
69
+ # set as the array of errors on whatever model is being updated. Since this
70
+ # framework makes validating on the client easy, it is rare that this will be
71
+ # needed.
72
+ #
73
+ # The client makes this array of errors available to all running controllers in
74
+ # the same manner as errors from client-side validation. In other words, your
75
+ # client code needs only one method for displaying errors to the user and can be
76
+ # agnostic as to whether the errors came from the client or the server.
77
+ def set_errors(errors)
78
+ @json[:errors] = errors.to_a
79
+ end
80
+
81
+ # Does the same thing as `set_errors` but will add to an existing array of errors
82
+ # if one exists instead of replacing it. This is what you should use if you
83
+ # are modifying more than one model and errors may come from multiple sources.
84
+ def append_errors(errors)
85
+ if @json[:errors]
86
+ @json[:errors] = @json[:errors].concat(errors.to_a)
87
+ else
88
+ @json[:errors] = errors.to_a
89
+ end
90
+ end
91
+
92
+ # Sets data to be held in the client model store cache for the named model.
93
+ #
94
+ # The `model_name` parameter should be a string in singular snake case.
95
+ #
96
+ # The `data` parameter should be either a single hash-like object, or an
97
+ # array-like object of hash-like objects. Array-like means it responds to
98
+ # `:collect`, which both true arrays and ActiveRecord collections do. Hash-like
99
+ # means it responds to `:to_hash`, or as a fallback `:as_json`. Single ActiveRecord
100
+ # records do respond to `:as_json` out of the box, but not `:to_hash`. If you
101
+ # provide a `to_hash` method in your model classes, you can explicitly set what
102
+ # data elements are sent to the client vs. which ones are excluded (eg. you
103
+ # probably don't want to send a password digest), and it allows you to send
104
+ # calculated values as well.
105
+ #
106
+ # The model data is MERGED into the cache on the client.
107
+ #
108
+ # It is appropriate to use this method when some subset of model entities have changed
109
+ # but the client is still holding other entities that do not need to be reloaded.
110
+ # This can save on bandwidth and load on the database.
111
+ #
112
+ def set_model_data(model_name, data)
113
+ warn "set_model_data is DEPRECATED!! Please use merge_model_data instead"
114
+ merge_model_data(model_name, data)
115
+ end
116
+
117
+ def merge_model_data(model_name, data)
118
+ obj = @json[:models][model_name] || {}
119
+
120
+ result = nil
121
+
122
+ if data.respond_to? :collect
123
+ if data.length > 0
124
+ if data[0].respond_to? :to_hash
125
+ result = data.collect {|a| a.to_hash }
126
+ else
127
+ result = data.collect {|a| a.as_json }
128
+ end
129
+ else
130
+ result = []
131
+ end
132
+ elsif data.respond_to? :to_hash
133
+ result = [data.to_hash]
134
+ else
135
+ result = [data.as_json]
136
+ end
137
+
138
+ if obj[:data]
139
+ obj[:data].concat result
140
+ else
141
+ obj[:data] = result
142
+ end
143
+
144
+ # Reassign it back. If we got a new hash, it isn't a reference from the @json
145
+ # object, so it won't be associated unless we make it so manually.
146
+ # If we did get a hash back on the first line, it is a reference, but since we
147
+ # merged into it, it is safe to reassign it back.
148
+ @json[:models][model_name] = obj
149
+
150
+ # Pass the data through. That way you can do an assignment on a fetch in
151
+ # one step
152
+ data
153
+ end
154
+
155
+ # This does the same thing as `merge_model_data` (in fact it defers to that method
156
+ # for converting the `data` parameter into the json format the client expects, so
157
+ # please read that documentation too), but also instructs the client to clear out
158
+ # a portion of the model store cache based on a set of foreign key values.
159
+ #
160
+ # The `foreign_keys` parameter is a hash, mapping the names of foreign keys on
161
+ # which to match with the corresponding values. For example, if we wanted to
162
+ # replace all the items on the cache with the ones we fetched for a particular
163
+ # user, we'd say:
164
+ # @mvcoffee.replace_model_data 'item', @items, user_id: @user.id
165
+ #
166
+ def set_model_replace_on(model_name, data, foreign_keys)
167
+ warn "set_model_replace_on is DEPRECATED!! Please use replace_model_data instead"
168
+ replace_model_data(model_name, data, foreign_keys)
169
+ end
170
+
171
+ def replace_model_data(model_name, data, foreign_keys = {})
172
+ merge_model_data(model_name, data)
173
+
174
+ # This is guaranteed to be non-nil after set_model_data has been called.
175
+ obj = @json[:models][model_name]
176
+
177
+ obj[:replace_on] = foreign_keys
178
+
179
+ # Reassign it back. If we got a new hash, it isn't a reference from the @json
180
+ # object, so it won't be associated unless we make it so manually.
181
+ # If we did get a hash back on the first line, it is a reference, but since we
182
+ # merged into it, it is safe to reassign it back.
183
+ @json[:models][model_name] = obj
184
+
185
+ # Pass the data through
186
+ data
187
+ end
188
+
189
+ # Instructs the client to delete certain records from the model store cache. This
190
+ # doesn't remove anything from the database, it just tells the cache to forget
191
+ # about some records. Most likely, the time you'd want to use this is after
192
+ # destroying records in the database to let the client know those records no longer
193
+ # exist.
194
+ #
195
+ # The `model_name` parameter should be a string in singular snake case.
196
+ #
197
+ # The `data` parameter is an array of the primary key id's for the records to be
198
+ # removed. Optionally, it can be just a single integer.
199
+ def set_model_delete(model_name, data)
200
+ obj = @json[:models][model_name] || {}
201
+
202
+ obj[:delete] ||= []
203
+ if data.respond_to? :to_a
204
+ obj[:delete] += data.to_a
205
+ else
206
+ obj[:delete] << data
207
+ end
208
+
209
+ @json[:models][model_name] = obj
210
+ end
211
+
212
+ #==============================================================================
213
+ #
214
+ # Convenience methods composed of the primitive methods above.
215
+ #
216
+ # These methods not only package into one step some of the most common things
217
+ # you're going to want to do, doing both the database action and the building of
218
+ # the JSON for the client in one go.
219
+ #
220
+
221
+ # Finds and returns a model record identified by the primary key `id`.
222
+ # It sets the into the client session the `id` of the current record,
223
+ # identified by the key `<table_name>_id`, where `table_name` is the singular
224
+ # snake case name of the model. It also sets the fetched record into the
225
+ # model data to be stored in the client Model Store cache.
226
+ #
227
+ # `model` is the class of the model to be fetched. For example, to fetch the
228
+ # Item with id = 42 and assign it to the instance variable `@item`, you'd say:
229
+ #
230
+ # @item = @mvcoffee.find Item, 42
231
+ #
232
+ # This sets @item to the Active Record Item with id = 42 and sets the client
233
+ # session key `"item_id"` to 42.
234
+ #
235
+ def find(model, id)
236
+ table_name = model.table_name.singularize
237
+ data = model.find id
238
+
239
+ set_session "#{table_name}_id" => id
240
+
241
+ merge_model_data table_name, data
242
+ end
243
+
244
+ # Finds and returns all records of the given model. It sets the fetched records
245
+ # into the model data to be stored in the client Model Store cache, replacing
246
+ # all records for this Model.
247
+ #
248
+ # `model` is the class of the model to be fetched. For example, to fetch all
249
+ # of the Categories, you'd say:
250
+ #
251
+ # @categories = @mvcoffee.all Category
252
+ #
253
+ def all(model)
254
+ table_name = model.table_name.singularize
255
+ data = model.all
256
+
257
+ replace_model_data table_name, data
258
+ end
259
+
260
+ # Fetches and returns all of the children records of the `entity` given following
261
+ # the given `has_many_of` association.
262
+ # It sets into the client session the `id` of the parent entity,
263
+ # identified by the key `<table_name>_id`, where `table_name` is the singular
264
+ # snake case of the parent model.
265
+ # It also sets the fetched records
266
+ # into the model data to be stored in the client Model Store cache, replacing
267
+ # all records for this Model that have a foreign_key matching the `id` of `entity`.
268
+ #
269
+ # `entity` is an Active Record record. `has_many_of` can be either a symbol or a
270
+ # string, and may be either plural or singular.
271
+ #
272
+ # For example, if you have a model Department that has many Items, given a
273
+ # department entity, you'd say:
274
+ #
275
+ # @items = @mvcoffee.fetch_has_many @department, :items
276
+ #
277
+ # This sets `@items` to `@department.items` and sets the client session key
278
+ # `"department_id"` to `@department.id`.
279
+ #
280
+ def fetch_has_many(entity, has_many_of)
281
+ table_name = has_many_of.to_s.singularize
282
+ child_name = table_name
283
+ method_call = table_name.pluralize.to_sym
284
+ childs_name = method_call
285
+ begin
286
+ options = entity.association(childs_name).reflection.options
287
+ if options and options[:through]
288
+ method_call = options[:through]
289
+ table_name = method_call.to_s.singularize
290
+ end
291
+ rescue
292
+ # Ignore
293
+ end
294
+
295
+ parent_table_name = entity.class.table_name.singularize
296
+ foreign_key = "#{parent_table_name}_id"
297
+
298
+ data = entity.send method_call
299
+
300
+ replace_on = { foreign_key => entity.id }
301
+
302
+ set_session replace_on
303
+
304
+ replace_model_data table_name, data, replace_on
305
+ end
306
+
307
+ # Destroys the given `entity` and communicates to the client to remove this
308
+ # record from the Data Store cache.
309
+ #
310
+ # It ends in an exclamation mark to warn you,
311
+ # **this really does delete the entity from the database**!
312
+ #
313
+ # `entity` is an Active Record record. For example, if you had an Item record
314
+ # stored in `@item`, this would call `destroy` on it and tell the client cache to
315
+ # do the same:
316
+ #
317
+ # @mvcoffee.delete! @item
318
+ #
319
+ def delete!(entity)
320
+ table_name = entity.class.table_name.singularize
321
+
322
+ entity.destroy
323
+
324
+ set_model_delete table_name, entity.id
325
+ end
326
+
327
+ #==============================================================================
328
+ #
329
+ # Automatically handle caching
330
+ #
331
+
332
+ # This does smart caching for you.
333
+ #
334
+ # Concrete example: Department has_many Item
335
+ # If you already have a @department (likely set by a before_action in your
336
+ # controller), you call
337
+ # @mvcoffee.refresh_has_many @department, :items
338
+ # and it will follow these steps.
339
+ # * Check if the #{has_many_of}_updated_at is > the session value
340
+ # * If so, do the same fetch as fetch_has_many
341
+ # and put the session value of the new updated_at
342
+ def refresh_has_many(entity, has_many_of)
343
+ table_name = has_many_of.to_s.singularize
344
+ child_name = table_name
345
+ method_call = table_name.pluralize.to_sym
346
+ childs_name = method_call
347
+ begin
348
+ options = entity.association(childs_name).reflection.options
349
+ if options and options[:through]
350
+ method_call = options[:through]
351
+ table_name = method_call.to_s.singularize
352
+ end
353
+ rescue
354
+ # Ignore
355
+ end
356
+
357
+ parent_table_name = entity.class.table_name.singularize
358
+ foreign_key = "#{parent_table_name}_id"
359
+
360
+ updated_at_call = "#{childs_name}_updated_at"
361
+ session_key = "#{parent_table_name}[#{child_name}[#{entity.id}]]"
362
+
363
+ server_age = nil
364
+
365
+ if entity.respond_to? updated_at_call
366
+ server_age = entity.send updated_at_call
367
+ end
368
+
369
+ stale = client_stale? session_key, server_age
370
+
371
+ if stale
372
+ data = entity.send method_call
373
+
374
+ replace_on = { foreign_key => entity.id }
375
+
376
+ set_session replace_on
377
+
378
+ server_age_hash = { session_key => server_age }
379
+ Rails.logger.info "-- MVCoffee -- Refresh has many: server age session message = #{server_age_hash}"
380
+ set_session server_age_hash
381
+
382
+ replace_model_data table_name, data, replace_on
383
+ else
384
+ # return an empty array if we didn't fetch anything fresh
385
+ []
386
+ end
387
+ end
388
+
389
+ def client_stale?(session_key, server_age)
390
+ client_age_string = client_session(session_key)
391
+ Rails.logger.info "-- MVCoffee -- client stale?: client age string = #{client_age_string}"
392
+
393
+ client_age = nil
394
+
395
+ begin
396
+ client_age = DateTime.parse(client_age_string)
397
+ rescue
398
+ # Ignore bad parse, just use nil
399
+ end
400
+
401
+
402
+ # The shortcutted or assignment here works, but doesn't allow us to log what's
403
+ # happening.
404
+ # stale = (
405
+ # client_age.nil? or
406
+ # server_age.nil? or
407
+ # server_age.to_datetime.to_s > client_age.utc.to_s
408
+ # )
409
+
410
+ stale = false
411
+ if client_age.nil?
412
+ Rails.logger.info "-- MVCoffee -- client stale?: client age is nil"
413
+ stale = true
414
+ elsif server_age.nil?
415
+ Rails.logger.info "-- MVCoffee -- client stale?: server age is nil"
416
+ stale = true
417
+ else
418
+ Rails.logger.info "-- MVCoffee -- client stale?: server age = #{server_age.to_datetime.utc}"
419
+ Rails.logger.info "-- MVCoffee -- client stale?: client age = #{client_age.utc}"
420
+ # Weird things happen if we just compare dates to dates. I think somewhere in
421
+ # there the millis are getting lost, and we really don't need to be _that_
422
+ # accurate. Odds are, if the client is stale, it's stale by minutes or days.
423
+ # The to_s is a cheap way to strip off millis and make sure we're comparing
424
+ # the same thing.
425
+ if server_age.to_datetime.utc.to_s > client_age.utc.to_s
426
+ Rails.logger.info "-- MVCoffee -- client stale?: server is newer, it's STALE"
427
+ stale = true
428
+ else
429
+ Rails.logger.info "-- MVCoffee -- client stale?: client is UP TO DATE"
430
+ end
431
+ end
432
+
433
+ stale
434
+ end
435
+
436
+ #==============================================================================
437
+ #
438
+ # Convert to JSON!
439
+ #
440
+
441
+ def to_json
442
+ @json.to_json
443
+ end
444
+
445
+ end
446
+ end
@@ -0,0 +1,80 @@
1
+ module ActiveRecord
2
+ class Base
3
+ def self.caches_via_mvcoffee(child, opts = {})
4
+ # This is a bunch of crazy metaprogramming!
5
+
6
+ # Ideally this will work now matter how they supply the child name.
7
+ # It could be a string or symbol, singular or plural.
8
+ # We normalize it to a plural string here, because that's what we need.
9
+ childs = child.to_s.pluralize
10
+
11
+ # This is the name of the method we need to define on this class to be called
12
+ # when a record of this model is created, and any time any child record is
13
+ # changed in any way
14
+ method = "#{childs}_updated!"
15
+
16
+ define_method method do
17
+ send "#{childs}_updated_at=", DateTime.now
18
+ save!
19
+ end
20
+
21
+ after_create method
22
+
23
+ # Okay, done changing this class, let's change the child class
24
+ #
25
+ # There are two cases here, the default, and if we are using :through.
26
+ # In the default case, there is only one descendent, the child, and it
27
+ # belongs_to this class, so its reference back to this class is through
28
+ # a singular method call.
29
+ #
30
+ # In the through case, we have two classes to deal with, the through class
31
+ # which is our direct descendent, and the "child" class we have many through
32
+ # the join table. In this case, the direct descendent should behave the way
33
+ # the child should in the usual case. It has a belongs_to singular reference
34
+ # back to this class. The "child" class should have a has_many through back to
35
+ # this class, so it will be a plural reference.
36
+
37
+ direct_descendent = child
38
+ if opts[:through]
39
+ direct_descendent = opts[:through]
40
+ end
41
+
42
+ # Create a reference to the child class definition.
43
+ # It doesn't have to be class_eval here, it could be regular eval, but I've read
44
+ # this is safer from code injection.
45
+ #
46
+ # Plus, doing this as a class_eval puts us in the same namespace as the parent.
47
+ clazz = class_eval "#{direct_descendent.to_s.singularize.camelcase}"
48
+
49
+ parent_name = table_name.singularize
50
+ refresh_method = "refresh_#{parent_name}_#{childs}_updated_at"
51
+ clazz.class_eval do
52
+ define_method refresh_method do
53
+ parent = send parent_name
54
+ parent.send method
55
+ end
56
+
57
+ after_save refresh_method
58
+ after_destroy refresh_method
59
+ end
60
+
61
+ # Now do the special case of a has_many through
62
+ if opts[:through]
63
+ clazz = class_eval "#{child.to_s.singularize.camelcase}"
64
+
65
+ parents_name = table_name.pluralize
66
+ clazz.class_eval do
67
+ define_method refresh_method do
68
+ parents = send parents_name
69
+ parents.each { |parent| parent.send method }
70
+ end
71
+
72
+ after_save refresh_method
73
+ # This may not be strictly necessary if delete cascade, but there's no harm
74
+ # in being thorough
75
+ after_destroy refresh_method
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,6 @@
1
+ module Mvcoffee
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Mvcoffee
2
+ module Rails
3
+ VERSION = "1.0.0"
4
+ end
5
+ end