mvcoffee-rails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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