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.
- 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
|