mvcoffee-rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/app/assets/javascripts/mvcoffee.js +1556 -0
- data/app/assets/javascripts/mvcoffee.min.js +2 -0
- data/app/helpers/mvcoffee_helper.rb +12 -0
- data/lib/mvcoffee/mvcoffee.rb +446 -0
- data/lib/mvcoffee/rails/active_record.rb +80 -0
- data/lib/mvcoffee/rails/engine.rb +6 -0
- data/lib/mvcoffee/rails/version.rb +5 -0
- data/lib/mvcoffee-rails.rb +146 -0
- metadata +98 -0
@@ -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,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
|