From 0f1df74f958f9bd071d3ede58a825625b26d4136 Mon Sep 17 00:00:00 2001 From: Ed Spencer Date: Thu, 20 Aug 2009 00:37:21 +0100 Subject: [PATCH] Added experimental methods to load an MVC app via an environment, plus the plugin builder script --- App.js | 54 +++++---- MVC.js | 145 ++++++++++++++++++++++++ controller/CrudController.js | 13 ++- ext-mvc-all-min.js | 2 +- ext-mvc-all.js | 212 ++++++++++++++++++++++++++++++----- scripts/plugin.rb | 186 +++++++++++++++++++++--------- scripts/templates/Model.js | 4 +- 7 files changed, 513 insertions(+), 103 deletions(-) diff --git a/App.js b/App.js index 8e27eda..7c18059 100644 --- a/App.js +++ b/App.js @@ -21,34 +21,40 @@ ExtMVC.App = Ext.extend(Ext.util.Observable, { this.initializeNamespaces(); - Ext.onReady(function() { - if (this.fireEvent('before-launch', this)) { - this.initializeRouter(); - // this.initializeViewport(); - this.initializeEvents(); + Ext.onReady(this.onReady, this); + }, + + /** + * @private + * Called when Ext.onReadt fires + */ + onReady: function() { + if (this.fireEvent('before-launch', this)) { + this.initializeRouter(); + // this.initializeViewport(); + this.initializeEvents(); - if (this.usesHistory === true) this.initializeHistory(); + if (this.usesHistory === true) this.initializeHistory(); - this.launch(); - this.fireEvent('launched', this); - - /** - * TODO: This used to reside in initializeHistory but this.launch() needs to be - * called before this dispatches so it is temporarily here... ugly though - */ - if (this.usesHistory) { - if (this.dispatchHistoryOnLoad === true) { - Ext.History.init(function(history) { - var hash = document.location.hash.replace("#", ""); - var params = this.router.recognise(hash); - if (params) {this.dispatch(params);} - }, this); - } else { - Ext.History.init(); - } + this.launch(); + this.fireEvent('launched', this); + + /** + * TODO: This used to reside in initializeHistory but this.launch() needs to be + * called before this dispatches so it is temporarily here... ugly though + */ + if (this.usesHistory) { + if (this.dispatchHistoryOnLoad === true) { + Ext.History.init(function(history) { + var hash = document.location.hash.replace("#", ""); + var params = this.router.recognise(hash); + if (params) {this.dispatch(params);} + }, this); + } else { + Ext.History.init(); } } - }, this); + } }, /** diff --git a/MVC.js b/MVC.js index d840231..8e60db7 100644 --- a/MVC.js +++ b/MVC.js @@ -46,6 +46,151 @@ ExtMVC = Ext.extend(Ext.util.Observable, { this.name = this.app.name; }, + /** + * @property bootParams + * @type Object + * An object which contains all boot parameters. These are used during the boot phase, + * and can be set using GET params after the '?' in the url + */ + bootParams: { + environment: 'production' + }, + + /** + * @property globalEnvironmentSettings + * @type Object + * All default environment settings that will be Ext.applyIf'd to the current environment. + * These are things that don't tend to change between applications, but you can override them if you need to + */ + globalEnvironmentSettings: { + pluginsDir : '../vendor/plugins', + libDir : '../lib', + configDir : '../config', + overridesDir: '../config/overrides', + appDir : '../app', + vendor : ['mvc'], + mvcFilename : 'ext-mvc-all-min', + config : ['initialize', 'database', 'routes'] + }, + + /** + * Boots up the application. + * TODO: When it works, document this :) + */ + boot: function() { + var args = window.location.href.split("?")[1]; + + /** + * Read config data from url parameters + */ + if (args != undefined) { + Ext.each(args.split("&"), function(arg) { + var splits = arg.split("="), + key = splits[0], + value = splits[1]; + + this.bootParams[key] = value; + }, this); + } + + + //load up the environment + Ext.Ajax.request({ + url: '../config/environment.json', + scope : this, + success: function(response, options) { + var envName = this.bootParams.environment; + + this.addEnvironmentSettings(envName, this.globalEnvironmentSettings); + this.addSettingsFromEnvironmentFile(response); + this.setCurrentEnvironment(envName); + + Ext.Ajax.request({ + url : String.format("../config/environments/{0}.json", envName), + success: function(response, options) { + this.addSettingsFromEnvironmentFile(response); + + this.onEnvironmentLoaded(this.getCurrentEnvironmentSettings()); + }, + scope : this + }); + }, + failure: function() { + Ext.Msg.alert( + 'Could not load environment', + 'The ' + config.environment + ' environment could not be found' + ); + } + }); + }, + + /** + * Called when the environment files have been loaded and application load can begin + * @param {Object} environment The current environment object + */ + onEnvironmentLoaded: function(env) { + var order = ['overrides', 'config', 'plugins', 'models', 'controllers', 'views'], + files = []; + + // Ext.each(order, function(fileList) { + // var dir = env[fileList + "Dir"] || String.format("../app/{0}", fileList); + // + // Ext.each(env[fileList], function(file) { + // files.push(String.format("{0}/{1}.js", dir, file)); + // }, this); + // }, this); + + Ext.each(env.overrides, function(file) { + files.push(String.format("{0}/{1}.js", env.overridesDir, file)); + }, this); + + Ext.each(env.config, function(file) { + files.push(String.format("{0}/{1}.js", env.configDir, file)); + }, this); + + Ext.each(env.plugins, function(file) { + files.push(String.format("{0}/{1}/{2}-all.js", env.pluginsDir, file, file)); + }, this); + + Ext.each(env.models, function(file) { + files.push(String.format("{0}/models/{1}.js", env.appDir, file)); + }, this); + + Ext.each(env.controllers, function(file) { + files.push(String.format("{0}/controllers/{1}Controller.js", env.appDir, file)); + }, this); + + Ext.iterate(env.views, function(dir, fileList) { + Ext.each(fileList, function(file) { + files.push(String.format("{0}/views/{1}/{2}.js", env.appDir, dir, file)); + }, this); + }, this); + + var fragment = ""; + for (var i=0; i < files.length; i++) { + fragment += String.format("", files[i]); + }; + + Ext.getBody().createChild(fragment); + + // Ext.onReady(function() { + // console.log('hmm'); + // console.log('test'); + // console.log(this); + // this.app.onReady(); + // }, this); + }, + + + /** + * @private + * Takes the response of an AJAX request, encodes it into a JSON object and adds to the current environment + */ + addSettingsFromEnvironmentFile: function(response) { + var envJSON = Ext.decode(response.responseText); + this.addEnvironmentSettings(this.bootParams.environment, envJSON); + }, + /** * @property controllers * When this.registerController('application', MyApp.ApplicationController) is called, diff --git a/controller/CrudController.js b/controller/CrudController.js index 944923f..3fe79d2 100644 --- a/controller/CrudController.js +++ b/controller/CrudController.js @@ -154,12 +154,16 @@ ExtMVC.controller.CrudController = Ext.extend(ExtMVC.controller.Controller, { * Renders the custom New view if present, otherwise falls back to the default scaffold New form */ build: function() { - return this.render('New', { + var buildView = this.render('New', { model : this.model, controller : this, listeners : this.getBuildViewListeners(), viewsPackage: this.viewsPackage }); + + this.onBuild(buildView); + + return buildView; }, /** @@ -326,6 +330,13 @@ ExtMVC.controller.CrudController = Ext.extend(ExtMVC.controller.Controller, { this.fireEvent('delete-failed', instance); }, + /** + * Called whenever the 'New' form has been rendered for a given instance. This is an empty function by default, + * which you can override to provide your own logic if needed + * @param {Ext.Component} form The rendered 'New' form + */ + onBuild: function(form) {}, + /** * Called whenever the Edit form has been rendered for a given instance. This is an empty function by default, * which you can override to provide your own logic if needed diff --git a/ext-mvc-all-min.js b/ext-mvc-all-min.js index 4d85fa2..343247a 100644 --- a/ext-mvc-all-min.js +++ b/ext-mvc-all-min.js @@ -1 +1 @@ -ExtMVC=Ext.extend(Ext.util.Observable,{version:"0.6b1",constructor:function(){ExtMVC.superclass.constructor.apply(this,arguments);this.addEvents("environment-changed");this.getEnvSettings=this.getCurrentEnvironmentSettings},setup:function(A){this.app=new ExtMVC.App(A);this.name=this.app.name},controllers:{},registerController:function(A,B){this.controllers[A]=B},getController:function(A){var B=this.controllers[A];if(B){if(typeof B==="function"){this.controllers[A]=new this.controllers[A]()}return this.controllers[A]}else{return null}},currentEnvironment:"production",environments:{production:{}},setCurrentEnvironment:function(A){if(this.getEnvironmentSettings(A)){this.currentEnvironment=A;this.fireEvent("environment-changed",A,this.getEnvironmentSettings(A))}},getCurrentEnvironment:function(){return ExtMVC.currentEnvironment},getCurrentEnvironmentSettings:function(){return this.getEnvironmentSettings(this.getCurrentEnvironment())},addEnvironmentSettings:function(B,A){ExtMVC.environments[B]=ExtMVC.environments[B]||{};Ext.apply(ExtMVC.environments[B],A)},getEnvironmentSettings:function(A){A=A||ExtMVC.environment;return ExtMVC.environments[A]}});ExtMVC=new ExtMVC();Ext.ns("ExtMVC.router","ExtMVC.plugin","ExtMVC.controller","ExtMVC.view","ExtMVC.view.scaffold","ExtMVC.lib");ExtMVC.App=Ext.extend(Ext.util.Observable,{constructor:function(A){ExtMVC.App.superclass.constructor.apply(this,arguments);Ext.apply(this,A||{});window[this.name]=this;this.initializeNamespaces();Ext.onReady(function(){if(this.fireEvent("before-launch",this)){this.initializeRouter();this.initializeEvents();if(this.usesHistory===true){this.initializeHistory()}this.launch();this.fireEvent("launched",this);if(this.usesHistory){if(this.dispatchHistoryOnLoad===true){Ext.History.init(function(C){var B=document.location.hash.replace("#","");var D=this.router.recognise(B);if(D){this.dispatch(D)}},this)}else{Ext.History.init()}}}},this)},name:"MyApp",usesHistory:false,dispatchHistoryOnLoad:true,launch:Ext.emptyFn,params:{},dispatch:function(B,C,A){var B=B||{};Ext.applyIf(B,{action:"index"});this.params=B;var E=ExtMVC.getController(B.controller);if(E!=undefined){var D=E[B.action];if(typeof D=="function"){D.apply(C||E,A||[])}else{throw new Error(String.format("Action '{0}' not found on Controller '{1}'",B.action,B.controller))}}},initializeRouter:function(){if(this.router==undefined){this.router=new ExtMVC.router.Router();ExtMVC.router.Router.defineRoutes(this.router)}},initializeNamespaces:function(A){var A=A||this.name;if(A){Ext.ns(A,A+".controllers",A+".models",A+".views")}},initializeHistory:function(){this.historyForm=Ext.getBody().createChild({tag:"form",action:"#",cls:"x-hidden",id:"history-form",children:[{tag:"div",children:[{tag:"input",id:"x-history-field",type:"hidden"},{tag:"iframe",id:"x-history-frame"}]}]});Ext.History.on("change",this.onHistoryChange,this)},onHistoryChange:function(B){var A=this.router.recognise(B);if(A){this.dispatch(A,null,[{url:B}])}},initializeEvents:function(){this.addEvents("before-launch","launched")}});ExtMVC.Inflector={Inflections:{plural:[[(/(quiz)$/i),"$1zes"],[(/^(ox)$/i),"$1en"],[(/([m|l])ouse$/i),"$1ice"],[(/(matr|vert|ind)ix|ex$/i),"$1ices"],[(/(x|ch|ss|sh)$/i),"$1es"],[(/([^aeiouy]|qu)y$/i),"$1ies"],[(/(hive)$/i),"$1s"],[(/(?:([^f])fe|([lr])f)$/i),"$1$2ves"],[(/sis$/i),"ses"],[(/([ti])um$/i),"$1a"],[(/(buffal|tomat)o$/i),"$1oes"],[(/(bu)s$/i),"$1ses"],[(/(alias|status)$/i),"$1es"],[(/(octop|vir)us$/i),"$1i"],[(/(ax|test)is$/i),"$1es"],[(/s$/i),"s"],[(/$/),"s"]],singular:[[(/(quiz)zes$/i),"$1"],[(/(matr)ices$/i),"$1ix"],[(/(vert|ind)ices$/i),"$1ex"],[(/^(ox)en/i),"$1"],[(/(alias|status)es$/i),"$1"],[(/(octop|vir)i$/i),"$1us"],[(/(cris|ax|test)es$/i),"$1is"],[(/(shoe)s$/i),"$1"],[(/(o)es$/i),"$1"],[(/(bus)es$/i),"$1"],[(/([m|l])ice$/i),"$1ouse"],[(/(x|ch|ss|sh)es$/i),"$1"],[(/(m)ovies$/i),"$1ovie"],[(/(s)eries$/i),"$1eries"],[(/([^aeiouy]|qu)ies$/i),"$1y"],[(/([lr])ves$/i),"$1f"],[(/(tive)s$/i),"$1"],[(/(hive)s$/i),"$1"],[(/([^f])ves$/i),"$1fe"],[(/(^analy)ses$/i),"$1sis"],[(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i),"$1$2sis"],[(/([ti])a$/i),"$1um"],[(/(n)ews$/i),"$1ews"],[(/s$/i),""]],irregular:[["move","moves"],["sex","sexes"],["child","children"],["man","men"],["person","people"]],uncountable:["sheep","fish","series","species","money","rice","information","equipment"]},ordinalize:function(A){if(11<=parseInt(A,10)%100&&parseInt(A,10)%100<=13){return A+"th"}else{switch(parseInt(A,10)%10){case 1:return A+"st";case 2:return A+"nd";case 3:return A+"rd";default:return A+"th"}}},pluralize:function(C){var E=ExtMVC.Inflector.uncountableOrIrregular(C);if(E){return E}for(var A=0;A/g,">").replace(/=0;C--){E[D[C].replace(":","")]=A[C]}for(option in this.options){E[option]=this.options[option]}return E},urlForNamed:function(A){var A=A||{};return this.urlFor(Ext.applyIf(A,this.options))},urlFor:function(C){var B=this.mappingString;for(var F in C){if(C[F]&&this.options[F]&&C[F]!=this.options[F]){return false}}var E=[];for(var F in C){E.push(":"+F)}E=E.sort();var A=this.paramsInStringWithOptions.sort();if(A.length!=E.length){return false}for(var D=0;D=0;B--){var C=this.conditions[E[B]];var D=String.format("({0})",C||"[a-zA-Z0-9_,]+");A=A.replace(new RegExp(E[B]),D)}return new RegExp("^"+A+"$")}};ExtMVC.lib.Dependencies=Ext.extend(Ext.util.Observable,{constructor:function(){this.dependencies={};ExtMVC.lib.Dependencies.superclass.constructor.apply(this,arguments)},get:function(A){return this.dependencies[A]||[]},add:function(D,C,B){var A=this.dependencies[D]||[];A.push({name:C,config:B});this.dependencies[D]=A}});Ext.extend=function(){var B=function(D){for(var C in D){this[C]=D[C]}};var A=Object.prototype.constructor;return function(J,G,I){if(Ext.isObject(G)){I=G;G=J;J=I.constructor!=A?I.constructor:function(){G.apply(this,arguments)}}var E=function(){},H,D=G.prototype;E.prototype=D;H=J.prototype=new E();H.constructor=J;J.superclass=D;if(D.constructor==A){D.constructor=G}J.override=function(F){Ext.override(J,F)};H.superclass=H.supr=(function(){return D});H.override=B;Ext.override(J,I);J.extend=function(F){Ext.extend(J,F)};var C=J.prototype.onExtended;if(C){C.call(J.prototype)}return J}}();ExtMVC.controller.Controller=Ext.extend(Ext.util.Observable,{name:null,onExtended:function(){if(this.name!=null){this.viewsPackage=Ext.ns(String.format("{0}.views.{1}",ExtMVC.name,this.name));ExtMVC.registerController(this.name,this.constructor)}},constructor:function(A){ExtMVC.controller.Controller.superclass.constructor.apply(this,arguments);Ext.apply(this,A||{});this.initEvents();this.initListeners()},initEvents:function(){},initListeners:function(){},showNotice:function(A){},getViewClass:function(A){return this.viewsPackage[A]},addTo:null,render:function(D,B){var C=this.getViewClass(D);if(typeof C=="function"){var A=new C(B);if(this.addTo){this.renderViaAddTo(A)}return A}else{throw new Error(String.format("View '{0}' not found",D))}},renderViaAddTo:function(A){if(this.addTo!=undefined){this.addTo.removeAll();this.addTo.doLayout();this.addTo.add(A);this.addTo.doLayout()}}});Ext.reg("controller",ExtMVC.controller.Controller);ExtMVC.controller.CrudController=Ext.extend(ExtMVC.controller.Controller,{model:null,create:function(C,B){var A=new this.model(C);A.save({scope:this,success:this.onCreateSuccess,failure:this.onCreateFailure})},read:function(A){this.model.findById(A,{scope:this,success:function(B){this.fireEvent("read",B)},failure:function(){this.fireEvent("read-failed",A)}})},update:function(A,C){for(var B in C){A.set(B,C[B])}A.save({scope:this,success:function(D){this.onUpdateSuccess(D,C)},failure:function(){this.onUpdateFailure(A,C)}})},destroy:function(A){if(A.destroy==undefined){var B={};B[this.model.prototype.primaryKey]=parseInt(A,10);var A=new (this.model)(B)}A.destroy({scope:this,success:this.onDestroySuccess,failure:this.onDestroyFailure})},index:function(){var A=this.render("Index",{model:this.model,controller:this,listeners:this.getIndexViewListeners(),viewsPackage:this.viewsPackage});this.fireEvent("index");return A},build:function(){return this.render("New",{model:this.model,controller:this,listeners:this.getBuildViewListeners(),viewsPackage:this.viewsPackage})},edit:function(A,B){B=B||{};if(A instanceof Ext.data.Record){Ext.applyIf(B,{model:this.model,controller:this,listeners:this.getEditViewListeners(),viewsPackage:this.viewsPackage,id:String.format("{0}_edit_{1}",this.name,A.get(A.primaryKey))});var D=this.render("Edit",B);D.loadRecord(A);this.onEdit(D,A);this.fireEvent("edit",A);return D}else{var C=A;this.model.find(parseInt(C,10),{scope:this,success:function(E){this.edit(E)}})}},getIndexViewListeners:function(){return{scope:this,"delete":this.destroy,"new":this.build,edit:this.edit}},getEditViewListeners:function(){return{scope:this,cancel:this.index,save:this.update}},getBuildViewListeners:function(){return{scope:this,cancel:this.index,save:this.create}},showCreatedNotice:function(){this.showNotice(String.format("{0} successfully created",this.model.prototype.singularHumanName))},showUpdatedNotice:function(){this.showNotice(String.format("{0} successfully updated",this.model.prototype.singularHumanName))},showDestroyedNotice:function(){this.showNotice(String.format("{0} successfully deleted",this.model.prototype.singularHumanName))},onCreateSuccess:function(A){if(this.fireEvent("create",A)!==false){this.showCreatedNotice();this.index()}},onCreateFailure:function(A){this.fireEvent("create-failed",A)},onUpdateSuccess:function(A,B){if(this.fireEvent("update",A,B)!==false){this.showUpdatedNotice();this.index()}},onUpdateFailure:function(A,B){this.fireEvent("update-failed",A,B)},onDestroySuccess:function(A){this.fireEvent("delete",A);this.showDestroyedNotice()},onDestroyFailure:function(A){this.fireEvent("delete-failed",A)},onEdit:function(A){},initEvents:function(){this.addEvents("create","create-failed","read","read-failed","update","update-failed","delete","delete-failed","index","edit")},getViewClass:function(B){var A=ExtMVC.controller.CrudController.superclass.getViewClass.call(this,B);return(A==undefined)?this.scaffoldViewName(B):A},scaffoldViewName:function(A){return ExtMVC.view.scaffold[A.titleize()]}});ExtMVC.model={pendingCreation:{},getModelsPendingDefinitionOf:function(A){return this.pendingCreation[A]||[]},setModelPendingDefinitionOf:function(B,D,C){var A=this.pendingCreation[B]||[];A.push({name:D,config:C});this.pendingCreation[B]=A},strictMode:false,modelNamespace:window,define:function(A,B){var D=true,B=B||{};if(typeof B.extend!="undefined"){var C=this.modelNamespace[B.extend];if(typeof C=="undefined"){D=false;this.setModelPendingDefinitionOf(B.extend,A,B)}}if(D){this.create.apply(this,arguments)}},create:function(B,E){E=E||{};if(this.isAlreadyDefined(B)){if(this.strictMode){throw new Error(B+" is already defined")}return false}var F=this.modelNamespace[E.extend];var A=this.buildFields(E.fields,F);delete E.fields;var D=this.modelNamespace[B]=Ext.data.Record.create(A);var G=E.classMethods||{};delete E.classMethods;Ext.apply(D.prototype,E);if(typeof F!="undefined"){Ext.applyIf(G,F);Ext.applyIf(D.prototype,F.prototype)}D.prototype.modelName=B;this.setupNames(D);for(var C in G){if(C!="prototype"){D[C]=G[C]}}this.initializePlugins(D);this.afterCreate(B)},afterCreate:function(A){var B=this.getModelsPendingDefinitionOf(A);if(B){Ext.each(B,function(C){this.create(C.name,C.config)},this)}},isAlreadyDefined:function(A){if(typeof this.modelNamespace[A]!="undefined"){return true}var C=false;for(superclass in this.pendingCreation){var B=this.pendingCreation[superclass];Ext.each(B,function(D){if(D.name==A){C=true}},this)}return C},buildFields:function(B,C){B=B||[];var A=new Ext.util.MixedCollection(false,function(D){return D.name});A.addAll(B);if(typeof C!="undefined"){C.prototype.fields.each(function(D){if(typeof A.get(D.name)=="undefined"){A.add(D)}})}return A.items},setupNames:function(A){var C=A.prototype,B=ExtMVC.Inflector;Ext.applyIf(A.prototype,{tableName:B.pluralize(C.modelName.underscore()),foreignKeyName:B.singularize(C.modelName.underscore())+"_id",singularHumanName:C.modelName.humanize().titleize(),pluralHumanName:B.pluralize(C.modelName.humanize().titleize())})},plugins:[],addPlugin:function(A){this.plugins.push(A)},initializePlugins:function(A){Ext.each(this.plugins,function(B){B.initialize(A)},this)}};Ext.ns("ExtMVC.model.plugin");ExtMVC.model.Base=function(){};ExtMVC.model.Base.prototype={primaryKey:"id",newRecord:function(){var A=this.get(this.primaryKey);return typeof A=="undefined"||A==""},MVCModelId:function(){return String.format("{0}-{1}",this.tableName,this.get(this.primaryKey))},getReader:function(){if(!this.reader){this.reader=new Ext.data.JsonReader({totalProperty:"results",root:this.tableName},this.constructor)}return this.reader},initialize:Ext.emptyFn};Ext.apply(Ext.data.Record.prototype,new ExtMVC.model.Base());ExtMVC.model.plugin.adapter={initialize:function(B){var A=new this.RESTJSONAdapter();Ext.override(Ext.data.Record,A.instanceMethods());Ext.apply(B,A.classMethods());try{Ext.override(ExtMVC.model.plugin.association.HasMany,A.hasManyAssociationMethods());Ext.override(ExtMVC.model.plugin.association.BelongsTo,A.belongsToAssociationMethods())}catch(C){}}};ExtMVC.model.addPlugin(ExtMVC.model.plugin.adapter);ExtMVC.model.plugin.adapter.Abstract=function(A){};ExtMVC.model.plugin.adapter.Abstract.prototype={doSave:Ext.emptyFn,doFind:Ext.emptyFn,doDestroy:Ext.emptyFn,instanceMethods:function(){return{adapter:this,save:function(A){A=A||{};if(A.skipValidation===true||this.isValid()){return this.adapter.doSave(this,A)}else{if(typeof A.failure=="function"){return A.failure.call(A.scope||this,this)}}},destroy:function(A){return this.adapter.doDestroy(this,A)},update:function(B,A){this.setValues(B);this.save(A)},loaded:function(){}}},classMethods:function(){return{adapter:this,create:function(C,B){var A=new this(C);A.save(B);return A},build:function(A){return new this(A)},find:function(B,A){if(typeof(B)=="number"){B={primaryKey:B}}return this.adapter.doFind(B,A,this)},destroy:function(B,A){return this.adapter.doDestroy(B,A,this)}}},hasManyAssociationMethods:function(){return{adapter:this,create:function(C,B){var A=new this.associatedClass(C)},build:function(B,A){},find:function(A){},loaded:function(){},destroy:function(A){}}},belongsToAssociationMethods:function(){return{adapter:this,find:function(A){},loaded:function(){},destroy:function(A){}}}};ExtMVC.model.plugin.adapter.RESTAdapter=Ext.extend(ExtMVC.model.plugin.adapter.Abstract,{createMethod:"POST",readMethod:"GET",updateMethod:"PUT",destroyMethod:"DELETE",proxyType:Ext.data.HttpProxy,doSave:function(B,D){if(typeof B=="undefined"){throw new Error("No instance provided to REST Adapter save")}D=D||{};var A=D.success||Ext.emptyFn,C=D.failure||Ext.emptyFn;delete D.success;delete D.failure;Ext.Ajax.request(Ext.apply({url:this.instanceUrl(B),method:B.newRecord()?this.createMethod:this.updateMethod,params:this.buildPostData(B),success:function(E,F,G){G=G||this;return function(H,I){var L=E.modelName.underscore(),K=Ext.decode(H.responseText)[L];for(var J in K){E.set(J,K[J])}F.call(this,E)}}(B,A,D.scope)},D))},afterSave:function(){},doFind:function(E,B,C){E=E||{};B=B||{};var F=(E.primaryKey!==undefined),A=B.url||this.findUrl(E,C);Ext.applyIf(B,{conditions:E,scope:this});var D=F?this.doSingleFind:this.doCollectionFind;return D.call(this,A,B,C)},doDestroy:function(B,C,D){var C=C||{};if(typeof B=="undefined"){throw new Error("No instance provided to REST Adapter destroy")}if(!(B instanceof Ext.data.Record)){var E=parseInt(B,10);B=new D();B.set(D.prototype.primaryKey,E)}var A=C.success||Ext.emptyFn;delete C.success;return Ext.Ajax.request(Ext.applyIf(C,{method:this.destroyMethod,url:this.instanceUrl(B),success:function(){A.call(C.scope||this,B)}}))},doSingleFind:function(D,C,F){var H=C.callback,A=C.success,G=C.failure;delete C.callback;delete C.success;delete C.failure;var E=this.decodeSingleLoadResponse;var B=function(J,I){if(typeof J=="function"){J.apply(C.scope,I)}};Ext.Ajax.request(Ext.apply(C,{callback:function(K,L,J){if(L===true){var I=new F(E(J.responseText,F));B(A,[I,K,J])}else{B(G,arguments)}B(H,arguments)}},this.buildProxyConfig(D)))},storeConfig:{autoLoad:true,remoteSort:false},doCollectionFind:function(B,A,C){Ext.applyIf(A,this.storeConfig);if(A.conditions!=undefined){Ext.applyIf(A,{baseParams:A.conditions})}return new Ext.data.Store(Ext.applyIf(A,{reader:C.prototype.getReader(),proxy:new this.proxyType(this.buildProxyConfig(B))}))},instanceUrl:function(A){if(A.newRecord()){return String.format("/{0}",A.tableName)}else{return String.format("/{0}/{1}",A.tableName,A.get(A.primaryKey))}},collectionUrl:function(A){return String.format("/{0}",A.prototype.tableName)},buildProxyConfig:function(A){return{url:A,method:this.readMethod}},buildPostData:function(A){var C={},B=A.modelName.underscore();for(key in A.data){C[B+"["+key+"]"]=A.data[key]}return C},decodeSingleLoadResponse:function(C,A){var B=A.prototype.tableName;return Ext.decode(C)[B]},findUrl:function(C,B){if(typeof(C)=="object"&&C.primaryKey){var A=new B({});A.set(A.primaryKey,C.primaryKey);delete C.primaryKey;return this.instanceUrl(A)}else{return this.collectionUrl(B)}}});ExtMVC.model.plugin.adapter.RESTJSONAdapter=Ext.extend(ExtMVC.model.plugin.adapter.RESTAdapter,{doSave:function(A,B){if(typeof A=="undefined"){throw new Error("No instance provided to REST Adapter save")}Ext.applyIf(B||{},{jsonData:A.data,params:{},headers:{"Content-Type":"application/json"}});ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.doSave.apply(this,arguments)},doDestroy:function(A,B,C){B=B||{};Ext.applyIf(B,{headers:{"Content-type":"application/json"}});ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.doDestroy.call(this,A,B,C)},decodeSingleLoadResponse:function(C,A){var B=ExtMVC.Inflector.singularize(A.prototype.tableName);return Ext.decode(C)[B]},buildProxyConfig:function(A){var B=ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.buildProxyConfig.apply(this,arguments);return Ext.apply(B,{headers:{"Content-Type":"application/json"}})}});Ext.ns("ExtMVC.model.plugin.validation");ExtMVC.model.plugin.validation.AbstractValidation=function(A,C,B){this.ownerClass=A;this.field=C;Ext.apply(this,B)};ExtMVC.model.plugin.validation.AbstractValidation.prototype={getValue:function(A){return A.get(this.field)},isValid:function(A){return true}};ExtMVC.model.plugin.validation.ValidatesPresenceOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{message:"must be present",isValid:function(A){var C=this.getValue(A),B=false;switch(typeof C){case"object":if(C!=null){B=true}break;case"string":if(C.length!=0){B=true}break}return B}});ExtMVC.model.plugin.validation.ValidatesLengthOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{tooShortMessage:"is too short",tooLongMessage:"is too long",message:"",isValid:function(A){var B=this.getValue(A);if(typeof B=="undefined"){return true}if(this.minimum&&B.lengththis.maximum){this.message=this.tooLongMessage;return false}return true}});ExtMVC.model.plugin.validation.ValidatesNumericalityOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{message:"must be a number",isValid:function(A){return"number"==typeof this.getValue(A)}});ExtMVC.model.plugin.validation.ValidatesInclusionOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{constructor:function(A,C,B){B=B||{};Ext.applyIf(B,{allowed:[]});ExtMVC.model.plugin.validation.ValidatesInclusionOf.superclass.constructor.call(this,A,C,B);Ext.applyIf(this,{message:"must be one of "+this.allowed.toSentence("or")})},isValid:function(A){var C=this.getValue(A);for(var B=0;B-1){E.push(this.buildColumn(F.name))}},this);Ext.each(A,function(F){if(this.preferredColumns.indexOf(F.name)==-1&&this.ignoreColumns.indexOf(F.name)==-1){E.push(this.buildColumn(F.name))}if(this.wideColumns.indexOf(F.name)){wideColumns.push(F.name)}},this);for(var D=E.length-1;D>=0;D--){var B=E[D];if(this.narrowColumns.indexOf(B.id)>-1){Ext.applyIf(B,{width:this.narrowColumnWidth})}else{if(this.wideColumns.indexOf(B.id)>-1){Ext.applyIf(B,{width:this.wideColumnWidth})}else{Ext.applyIf(B,{width:this.normalColumnWidth})}}}}return E},getFields:function(){if(this.useColumns===undefined){return this.model.prototype.fields.items}else{var A=[];Ext.each(this.useColumns,function(B){A.push({name:B})},this);return A}},buildColumn:function(A){var A=A||{};if(typeof(A)=="string"){A={name:A}}return Ext.applyIf(A,{id:A.name,header:A.name.replace(/_/g," ").titleize(),sortable:true,dataIndex:A.name})},hasAddButton:true,hasEditButton:true,hasDeleteButton:true,buildAddButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"New "+this.model.prototype.singularHumanName,scope:this,iconCls:"add",handler:this.onAdd}))},buildEditButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"Edit selected",scope:this,iconCls:"edit",disabled:true,handler:this.onEdit}))},buildDeleteButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"Delete selected",disabled:true,scope:this,iconCls:"delete",handler:this.onDelete}))},buildTopToolbar:function(){var A=[];if(this.hasAddButton===true){this.addButton=this.buildAddButton();A.push(this.addButton,"-")}if(this.hasEditButton===true){this.editButton=this.buildEditButton();A.push(this.editButton,"-")}if(this.hasDeleteButton===true){this.deleteButton=this.buildDeleteButton();A.push(this.deleteButton,"-")}if(this.hasSearchField===true){this.searchField=this.buildSearchField();A.push(this.searchField,"-")}this.getSelectionModel().on("selectionchange",function(B){if(B.getCount()>0){if(this.deleteButton!=undefined){this.deleteButton.enable()}if(this.editButton!=undefined){this.editButton.enable()}}else{if(this.deleteButton!=undefined){this.deleteButton.disable()}if(this.editButton!=undefined){this.editButton.disable()}}},this);return A},pageSize:25,buildBottomToolbar:function(A){return new Ext.PagingToolbar({store:A,displayInfo:true,pageSize:this.pageSize,emptyMsg:String.format("No {0} to display",this.model.prototype.pluralHumanName)})},hasSearchField:false,searchParamName:"q",buildSearchField:function(){this.searchField=new Ext.form.TwinTriggerField({width:200,validationEvent:false,validateOnBlur:false,hideTrigger1:true,onTrigger1Click:this.clearSearchField.createDelegate(this,[]),onTrigger2Click:this.onSearch.createDelegate(this,[]),trigger1Class:"x-form-clear-trigger",trigger2Class:"x-form-search-trigger"});this.searchField.on("specialkey",function(B,A){if(A.getKey()===A.ESC){this.clearSearchField()}A.stopEvent();if(A.getKey()===A.ENTER){this.onSearch()}},this);return this.searchField},clearSearchField:function(){var A=this.searchField;A.el.dom.value="";A.triggers[0].hide();this.doSearch()},onSearch:function(){var B=this.searchField,A=B.getRawValue();if(A.length<1){this.clearSearchField()}else{B.triggers[0].show();this.doSearch(A)}},doSearch:function(A){A=A||this.searchField.getRawValue()||"";var B={start:0};this.store.baseParams=this.store.baseParams||{};this.store.baseParams[this.searchParamName]=A;this.store.reload({params:B})},onAdd:function(){this.fireEvent("new")},onEdit:function(B){var A=this.getSelectionModel().getSelected();if(A){this.fireEvent("edit",A)}},onDelete:function(){Ext.Msg.confirm("Are you sure?",String.format("Are you sure you want to delete this {0}? This cannot be undone.",this.model.prototype.modelName.titleize()),function(A){if(A=="yes"){var B=this.getSelectionModel().getSelected();if(B){this.fireEvent("delete",B)}}},this)}});Ext.reg("scaffold_index",ExtMVC.view.scaffold.Index);ExtMVC.view.scaffold.New=Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel,{initComponent:function(){Ext.applyIf(this,{title:"New "+this.model.prototype.singularHumanName});ExtMVC.view.scaffold.New.superclass.initComponent.apply(this,arguments)}});Ext.reg("scaffold_new",ExtMVC.view.scaffold.New);ExtMVC.view.scaffold.Edit=Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel,{initComponent:function(){Ext.applyIf(this,{title:"Edit "+this.model.prototype.singularHumanName});ExtMVC.view.scaffold.Edit.superclass.initComponent.apply(this,arguments)},loadRecord:function(A){this.instance=A;this.getForm().loadRecord(A)},onSave:function(){this.fireEvent("save",this.instance,this.getFormValues(),this)}});Ext.reg("scaffold_edit",ExtMVC.view.scaffold.Edit);ExtMVC.view.HasManyEditorGridPanel=Ext.extend(Ext.grid.EditorGridPanel,{initComponent:function(){Ext.applyIf(this,{autoScroll:true,store:this.association.findAll(),viewConfig:{forceFit:true}});if(this.hasTopToolbar){this.addTopToolbar()}ExtMVC.view.HasManyEditorGridPanel.superclass.initComponent.apply(this,arguments);this.on("afteredit",function(A){A.record.save({success:function(){A.record.commit()}})},this);this.getSelectionModel().on("selectionchange",function(A,B){if(this.deleteButton){this.deleteButton.enable()}},this)},hasTopToolbar:true,hasNewButton:true,hasDeleteButton:true,addTopToolbar:function(B){var A=[];if(this.hasNewButton){this.newButton=new Ext.Toolbar.Button({iconCls:"add",text:"Add",scope:this,handler:this.onAdd});A.push(this.newButton);A.push("-")}if(this.hasDeleteButton){this.deleteButton=new Ext.Toolbar.Button({text:"Remove selected",disabled:true,iconCls:"delete",scope:this,handler:this.onDelete});A.push(this.deleteButton)}Ext.applyIf(this,{tbar:A})},windowConfig:{},onAdd:function(A){if(!this.addWindow){this.addWindow=new Ext.Window(Ext.applyIf(this.windowConfig,{title:"New",layout:"fit",modal:true,height:300,width:400,items:[this.form],closeAction:"hide",buttons:[{text:"Save",iconCls:"save",scope:this,handler:this.onSaveNew},{text:"Cancel",iconCls:"cancel",scope:this,handler:this.onCancelNew}]}))}this.addWindow.show()},onDelete:function(B){var A=this.getSelectionModel().selection.record;if(A){A.destroy({scope:this,success:function(){this.store.reload()},failure:function(){Ext.Msg.alert("Delete failed","Something went wrong while trying to delete - please try again");this.store.reload()}})}this.deleteButton.disable()},onSaveNew:function(){this.association.create(this.form.getForm().getValues(),{scope:this,success:function(B,A){this.store.reload();this.addWindow.hide()},failure:function(B,A){this.form.getForm().clearInvalid();this.form.getForm().markInvalid(B.errors.forForm())}})},onCancelNew:function(A){this.addWindow.hide()}});Ext.reg("hasmany_editorgrid",ExtMVC.view.HasManyEditorGridPanel);ExtMVC.view.FormWindow=Ext.extend(Ext.Window,{modal:true,height:230,width:400,initComponent:function(){this.form=this.buildForm();Ext.apply(this,{items:[this.form],buttons:[{text:"Save",iconCls:"save",scope:this,handler:this.onSave},{text:"Cancel",iconCls:"cancel",scope:this,handler:this.onCancel}],layout:"fit",closeAction:"hide"});ExtMVC.view.FormWindow.superclass.initComponent.apply(this,arguments)},buildForm:function(){return new Ext.form.FormPanel({})},loadRecord:function(A){this.instance=A;this.form.form.loadRecord(A)},onSave:function(){this.fireEvent("save",this.getFormValues(),this)},onCancel:function(){this.hide()},getFormValues:function(){var B=this.form.getForm(),A={};B.items.each(function(D){var C=(typeof D.getSubmitValue=="function")?"getSubmitValue":"getValue";A[D.getName()]=D[C]()},this);return A}});Ext.reg("formwindow",ExtMVC.view.FormWindow); \ No newline at end of file +ExtMVC=Ext.extend(Ext.util.Observable,{version:"0.6b1",constructor:function(){ExtMVC.superclass.constructor.apply(this,arguments);this.addEvents("environment-changed");this.getEnvSettings=this.getCurrentEnvironmentSettings},setup:function(A){this.app=new ExtMVC.App(A);this.name=this.app.name},bootParams:{environment:"production"},globalEnvironmentSettings:{pluginsDir:"../vendor/plugins",libDir:"../lib",configDir:"../config",overridesDir:"../config/overrides",appDir:"../app",vendor:["mvc"],mvcFilename:"ext-mvc-all-min",config:["initialize","database","routes"]},boot:function(){var A=window.location.href.split("?")[1];if(A!=undefined){Ext.each(A.split("&"),function(B){var E=B.split("="),C=E[0],D=E[1];this.bootParams[C]=D},this)}Ext.Ajax.request({url:"../config/environment.json",scope:this,success:function(B,C){var D=this.bootParams.environment;this.addEnvironmentSettings(D,this.globalEnvironmentSettings);this.addSettingsFromEnvironmentFile(B);this.setCurrentEnvironment(D);Ext.Ajax.request({url:String.format("../config/environments/{0}.json",D),success:function(E,F){this.addSettingsFromEnvironmentFile(E);this.onEnvironmentLoaded(this.getCurrentEnvironmentSettings())},scope:this})},failure:function(){Ext.Msg.alert("Could not load environment","The "+config.environment+" environment could not be found")}})},onEnvironmentLoaded:function(D){var A=["overrides","config","plugins","models","controllers","views"],E=[];Ext.each(D.overrides,function(F){E.push(String.format("{0}/{1}.js",D.overridesDir,F))},this);Ext.each(D.config,function(F){E.push(String.format("{0}/{1}.js",D.configDir,F))},this);Ext.each(D.plugins,function(F){E.push(String.format("{0}/{1}/{2}-all.js",D.pluginsDir,F,F))},this);Ext.each(D.models,function(F){E.push(String.format("{0}/models/{1}.js",D.appDir,F))},this);Ext.each(D.controllers,function(F){E.push(String.format("{0}/controllers/{1}Controller.js",D.appDir,F))},this);Ext.iterate(D.views,function(G,F){Ext.each(F,function(H){E.push(String.format("{0}/views/{1}/{2}.js",D.appDir,G,H))},this)},this);var B="";for(var C=0;C<\/script>',E[C])}Ext.getBody().createChild(B)},addSettingsFromEnvironmentFile:function(A){var B=Ext.decode(A.responseText);this.addEnvironmentSettings(this.bootParams.environment,B)},controllers:{},registerController:function(A,B){this.controllers[A]=B},getController:function(A){var B=this.controllers[A];if(B){if(typeof B==="function"){this.controllers[A]=new this.controllers[A]()}return this.controllers[A]}else{return null}},currentEnvironment:"production",environments:{production:{}},setCurrentEnvironment:function(A){if(this.getEnvironmentSettings(A)){this.currentEnvironment=A;this.fireEvent("environment-changed",A,this.getEnvironmentSettings(A))}},getCurrentEnvironment:function(){return ExtMVC.currentEnvironment},getCurrentEnvironmentSettings:function(){return this.getEnvironmentSettings(this.getCurrentEnvironment())},addEnvironmentSettings:function(B,A){ExtMVC.environments[B]=ExtMVC.environments[B]||{};Ext.apply(ExtMVC.environments[B],A)},getEnvironmentSettings:function(A){A=A||ExtMVC.environment;return ExtMVC.environments[A]}});ExtMVC=new ExtMVC();Ext.ns("ExtMVC.router","ExtMVC.plugin","ExtMVC.controller","ExtMVC.view","ExtMVC.view.scaffold","ExtMVC.lib");ExtMVC.App=Ext.extend(Ext.util.Observable,{constructor:function(A){ExtMVC.App.superclass.constructor.apply(this,arguments);Ext.apply(this,A||{});window[this.name]=this;this.initializeNamespaces();Ext.onReady(this.onReady,this)},onReady:function(){if(this.fireEvent("before-launch",this)){this.initializeRouter();this.initializeEvents();if(this.usesHistory===true){this.initializeHistory()}this.launch();this.fireEvent("launched",this);if(this.usesHistory){if(this.dispatchHistoryOnLoad===true){Ext.History.init(function(B){var A=document.location.hash.replace("#","");var C=this.router.recognise(A);if(C){this.dispatch(C)}},this)}else{Ext.History.init()}}}},name:"MyApp",usesHistory:false,dispatchHistoryOnLoad:true,launch:Ext.emptyFn,params:{},dispatch:function(B,C,A){var B=B||{};Ext.applyIf(B,{action:"index"});this.params=B;var E=ExtMVC.getController(B.controller);if(E!=undefined){var D=E[B.action];if(typeof D=="function"){D.apply(C||E,A||[])}else{throw new Error(String.format("Action '{0}' not found on Controller '{1}'",B.action,B.controller))}}},initializeRouter:function(){if(this.router==undefined){this.router=new ExtMVC.router.Router();ExtMVC.router.Router.defineRoutes(this.router)}},initializeNamespaces:function(A){var A=A||this.name;if(A){Ext.ns(A,A+".controllers",A+".models",A+".views")}},initializeHistory:function(){this.historyForm=Ext.getBody().createChild({tag:"form",action:"#",cls:"x-hidden",id:"history-form",children:[{tag:"div",children:[{tag:"input",id:"x-history-field",type:"hidden"},{tag:"iframe",id:"x-history-frame"}]}]});Ext.History.on("change",this.onHistoryChange,this)},onHistoryChange:function(B){var A=this.router.recognise(B);if(A){this.dispatch(A,null,[{url:B}])}},initializeEvents:function(){this.addEvents("before-launch","launched")}});ExtMVC.Inflector={Inflections:{plural:[[(/(quiz)$/i),"$1zes"],[(/^(ox)$/i),"$1en"],[(/([m|l])ouse$/i),"$1ice"],[(/(matr|vert|ind)ix|ex$/i),"$1ices"],[(/(x|ch|ss|sh)$/i),"$1es"],[(/([^aeiouy]|qu)y$/i),"$1ies"],[(/(hive)$/i),"$1s"],[(/(?:([^f])fe|([lr])f)$/i),"$1$2ves"],[(/sis$/i),"ses"],[(/([ti])um$/i),"$1a"],[(/(buffal|tomat)o$/i),"$1oes"],[(/(bu)s$/i),"$1ses"],[(/(alias|status)$/i),"$1es"],[(/(octop|vir)us$/i),"$1i"],[(/(ax|test)is$/i),"$1es"],[(/s$/i),"s"],[(/$/),"s"]],singular:[[(/(quiz)zes$/i),"$1"],[(/(matr)ices$/i),"$1ix"],[(/(vert|ind)ices$/i),"$1ex"],[(/^(ox)en/i),"$1"],[(/(alias|status)es$/i),"$1"],[(/(octop|vir)i$/i),"$1us"],[(/(cris|ax|test)es$/i),"$1is"],[(/(shoe)s$/i),"$1"],[(/(o)es$/i),"$1"],[(/(bus)es$/i),"$1"],[(/([m|l])ice$/i),"$1ouse"],[(/(x|ch|ss|sh)es$/i),"$1"],[(/(m)ovies$/i),"$1ovie"],[(/(s)eries$/i),"$1eries"],[(/([^aeiouy]|qu)ies$/i),"$1y"],[(/([lr])ves$/i),"$1f"],[(/(tive)s$/i),"$1"],[(/(hive)s$/i),"$1"],[(/([^f])ves$/i),"$1fe"],[(/(^analy)ses$/i),"$1sis"],[(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i),"$1$2sis"],[(/([ti])a$/i),"$1um"],[(/(n)ews$/i),"$1ews"],[(/s$/i),""]],irregular:[["move","moves"],["sex","sexes"],["child","children"],["man","men"],["person","people"]],uncountable:["sheep","fish","series","species","money","rice","information","equipment"]},ordinalize:function(A){if(11<=parseInt(A,10)%100&&parseInt(A,10)%100<=13){return A+"th"}else{switch(parseInt(A,10)%10){case 1:return A+"st";case 2:return A+"nd";case 3:return A+"rd";default:return A+"th"}}},pluralize:function(C){var E=ExtMVC.Inflector.uncountableOrIrregular(C);if(E){return E}for(var A=0;A/g,">").replace(/=0;C--){E[D[C].replace(":","")]=A[C]}for(option in this.options){E[option]=this.options[option]}return E},urlForNamed:function(A){var A=A||{};return this.urlFor(Ext.applyIf(A,this.options))},urlFor:function(C){var B=this.mappingString;for(var F in C){if(C[F]&&this.options[F]&&C[F]!=this.options[F]){return false}}var E=[];for(var F in C){E.push(":"+F)}E=E.sort();var A=this.paramsInStringWithOptions.sort();if(A.length!=E.length){return false}for(var D=0;D=0;B--){var C=this.conditions[E[B]];var D=String.format("({0})",C||"[a-zA-Z0-9_,]+");A=A.replace(new RegExp(E[B]),D)}return new RegExp("^"+A+"$")}};ExtMVC.lib.Dependencies=Ext.extend(Ext.util.Observable,{constructor:function(){this.dependencies={};ExtMVC.lib.Dependencies.superclass.constructor.apply(this,arguments)},get:function(A){return this.dependencies[A]||[]},add:function(D,C,B){var A=this.dependencies[D]||[];A.push({name:C,config:B});this.dependencies[D]=A}});Ext.extend=function(){var B=function(D){for(var C in D){this[C]=D[C]}};var A=Object.prototype.constructor;return function(J,G,I){if(Ext.isObject(G)){I=G;G=J;J=I.constructor!=A?I.constructor:function(){G.apply(this,arguments)}}var E=function(){},H,D=G.prototype;E.prototype=D;H=J.prototype=new E();H.constructor=J;J.superclass=D;if(D.constructor==A){D.constructor=G}J.override=function(F){Ext.override(J,F)};H.superclass=H.supr=(function(){return D});H.override=B;Ext.override(J,I);J.extend=function(F){Ext.extend(J,F)};var C=J.prototype.onExtended;if(C){C.call(J.prototype)}return J}}();ExtMVC.controller.Controller=Ext.extend(Ext.util.Observable,{name:null,onExtended:function(){if(this.name!=null){this.viewsPackage=Ext.ns(String.format("{0}.views.{1}",ExtMVC.name,this.name));ExtMVC.registerController(this.name,this.constructor)}},constructor:function(A){ExtMVC.controller.Controller.superclass.constructor.apply(this,arguments);Ext.apply(this,A||{});this.initEvents();this.initListeners()},initEvents:function(){},initListeners:function(){},showNotice:function(A){},getViewClass:function(A){return this.viewsPackage[A]},addTo:null,render:function(D,B){var C=this.getViewClass(D);if(typeof C=="function"){var A=new C(B);if(this.addTo){this.renderViaAddTo(A)}return A}else{throw new Error(String.format("View '{0}' not found",D))}},renderViaAddTo:function(A){if(this.addTo!=undefined){this.addTo.removeAll();this.addTo.doLayout();this.addTo.add(A);this.addTo.doLayout()}}});Ext.reg("controller",ExtMVC.controller.Controller);ExtMVC.controller.CrudController=Ext.extend(ExtMVC.controller.Controller,{model:null,create:function(C,B){var A=new this.model(C);A.save({scope:this,success:this.onCreateSuccess,failure:this.onCreateFailure})},read:function(A){this.model.findById(A,{scope:this,success:function(B){this.fireEvent("read",B)},failure:function(){this.fireEvent("read-failed",A)}})},update:function(A,C){for(var B in C){A.set(B,C[B])}A.save({scope:this,success:function(D){this.onUpdateSuccess(D,C)},failure:function(){this.onUpdateFailure(A,C)}})},destroy:function(A){if(A.destroy==undefined){var B={};B[this.model.prototype.primaryKey]=parseInt(A,10);var A=new (this.model)(B)}A.destroy({scope:this,success:this.onDestroySuccess,failure:this.onDestroyFailure})},index:function(){var A=this.render("Index",{model:this.model,controller:this,listeners:this.getIndexViewListeners(),viewsPackage:this.viewsPackage});this.fireEvent("index");return A},build:function(){var A=this.render("New",{model:this.model,controller:this,listeners:this.getBuildViewListeners(),viewsPackage:this.viewsPackage});this.onBuild(A);return A},edit:function(A,B){B=B||{};if(A instanceof Ext.data.Record){Ext.applyIf(B,{model:this.model,controller:this,listeners:this.getEditViewListeners(),viewsPackage:this.viewsPackage,id:String.format("{0}_edit_{1}",this.name,A.get(A.primaryKey))});var D=this.render("Edit",B);D.loadRecord(A);this.onEdit(D,A);this.fireEvent("edit",A);return D}else{var C=A;this.model.find(parseInt(C,10),{scope:this,success:function(E){this.edit(E)}})}},getIndexViewListeners:function(){return{scope:this,"delete":this.destroy,"new":this.build,edit:this.edit}},getEditViewListeners:function(){return{scope:this,cancel:this.index,save:this.update}},getBuildViewListeners:function(){return{scope:this,cancel:this.index,save:this.create}},showCreatedNotice:function(){this.showNotice(String.format("{0} successfully created",this.model.prototype.singularHumanName))},showUpdatedNotice:function(){this.showNotice(String.format("{0} successfully updated",this.model.prototype.singularHumanName))},showDestroyedNotice:function(){this.showNotice(String.format("{0} successfully deleted",this.model.prototype.singularHumanName))},onCreateSuccess:function(A){if(this.fireEvent("create",A)!==false){this.showCreatedNotice();this.index()}},onCreateFailure:function(A){this.fireEvent("create-failed",A)},onUpdateSuccess:function(A,B){if(this.fireEvent("update",A,B)!==false){this.showUpdatedNotice();this.index()}},onUpdateFailure:function(A,B){this.fireEvent("update-failed",A,B)},onDestroySuccess:function(A){this.fireEvent("delete",A);this.showDestroyedNotice()},onDestroyFailure:function(A){this.fireEvent("delete-failed",A)},onBuild:function(A){},onEdit:function(A){},initEvents:function(){this.addEvents("create","create-failed","read","read-failed","update","update-failed","delete","delete-failed","index","edit")},getViewClass:function(B){var A=ExtMVC.controller.CrudController.superclass.getViewClass.call(this,B);return(A==undefined)?this.scaffoldViewName(B):A},scaffoldViewName:function(A){return ExtMVC.view.scaffold[A.titleize()]}});ExtMVC.model={pendingCreation:{},getModelsPendingDefinitionOf:function(A){return this.pendingCreation[A]||[]},setModelPendingDefinitionOf:function(B,D,C){var A=this.pendingCreation[B]||[];A.push({name:D,config:C});this.pendingCreation[B]=A},strictMode:false,modelNamespace:window,define:function(A,B){var D=true,B=B||{};if(typeof B.extend!="undefined"){var C=this.modelNamespace[B.extend];if(typeof C=="undefined"){D=false;this.setModelPendingDefinitionOf(B.extend,A,B)}}if(D){this.create.apply(this,arguments)}},create:function(B,E){E=E||{};if(this.isAlreadyDefined(B)){if(this.strictMode){throw new Error(B+" is already defined")}return false}var F=this.modelNamespace[E.extend];var A=this.buildFields(E.fields,F);delete E.fields;var D=this.modelNamespace[B]=Ext.data.Record.create(A);var G=E.classMethods||{};delete E.classMethods;Ext.apply(D.prototype,E);if(typeof F!="undefined"){Ext.applyIf(G,F);Ext.applyIf(D.prototype,F.prototype)}D.prototype.modelName=B;this.setupNames(D);for(var C in G){if(C!="prototype"){D[C]=G[C]}}this.initializePlugins(D);this.afterCreate(B)},afterCreate:function(A){var B=this.getModelsPendingDefinitionOf(A);if(B){Ext.each(B,function(C){this.create(C.name,C.config)},this)}},isAlreadyDefined:function(A){if(typeof this.modelNamespace[A]!="undefined"){return true}var C=false;for(superclass in this.pendingCreation){var B=this.pendingCreation[superclass];Ext.each(B,function(D){if(D.name==A){C=true}},this)}return C},buildFields:function(B,C){B=B||[];var A=new Ext.util.MixedCollection(false,function(D){return D.name});A.addAll(B);if(typeof C!="undefined"){C.prototype.fields.each(function(D){if(typeof A.get(D.name)=="undefined"){A.add(D)}})}return A.items},setupNames:function(A){var C=A.prototype,B=ExtMVC.Inflector;Ext.applyIf(A.prototype,{tableName:B.pluralize(C.modelName.underscore()),foreignKeyName:B.singularize(C.modelName.underscore())+"_id",singularHumanName:C.modelName.humanize().titleize(),pluralHumanName:B.pluralize(C.modelName.humanize().titleize())})},plugins:[],addPlugin:function(A){this.plugins.push(A)},initializePlugins:function(A){Ext.each(this.plugins,function(B){B.initialize(A)},this)}};Ext.ns("ExtMVC.model.plugin");ExtMVC.model.Base=function(){};ExtMVC.model.Base.prototype={primaryKey:"id",newRecord:function(){var A=this.get(this.primaryKey);return typeof A=="undefined"||A==""},MVCModelId:function(){return String.format("{0}-{1}",this.tableName,this.get(this.primaryKey))},getReader:function(){if(!this.reader){this.reader=new Ext.data.JsonReader({totalProperty:"results",root:this.tableName},this.constructor)}return this.reader},initialize:Ext.emptyFn};Ext.apply(Ext.data.Record.prototype,new ExtMVC.model.Base());ExtMVC.model.plugin.adapter={initialize:function(B){var A=new this.RESTJSONAdapter();Ext.override(Ext.data.Record,A.instanceMethods());Ext.apply(B,A.classMethods());try{Ext.override(ExtMVC.model.plugin.association.HasMany,A.hasManyAssociationMethods());Ext.override(ExtMVC.model.plugin.association.BelongsTo,A.belongsToAssociationMethods())}catch(C){}}};ExtMVC.model.addPlugin(ExtMVC.model.plugin.adapter);ExtMVC.model.plugin.adapter.Abstract=function(A){};ExtMVC.model.plugin.adapter.Abstract.prototype={doSave:Ext.emptyFn,doFind:Ext.emptyFn,doDestroy:Ext.emptyFn,instanceMethods:function(){return{adapter:this,save:function(A){A=A||{};if(A.skipValidation===true||this.isValid()){return this.adapter.doSave(this,A)}else{if(typeof A.failure=="function"){return A.failure.call(A.scope||this,this)}}},destroy:function(A){return this.adapter.doDestroy(this,A)},update:function(B,A){this.setValues(B);this.save(A)},loaded:function(){}}},classMethods:function(){return{adapter:this,create:function(C,B){var A=new this(C);A.save(B);return A},build:function(A){return new this(A)},find:function(B,A){if(typeof(B)=="number"){B={primaryKey:B}}return this.adapter.doFind(B,A,this)},destroy:function(B,A){return this.adapter.doDestroy(B,A,this)}}},hasManyAssociationMethods:function(){return{adapter:this,create:function(C,B){var A=new this.associatedClass(C)},build:function(B,A){},find:function(A){},loaded:function(){},destroy:function(A){}}},belongsToAssociationMethods:function(){return{adapter:this,find:function(A){},loaded:function(){},destroy:function(A){}}}};ExtMVC.model.plugin.adapter.RESTAdapter=Ext.extend(ExtMVC.model.plugin.adapter.Abstract,{createMethod:"POST",readMethod:"GET",updateMethod:"PUT",destroyMethod:"DELETE",proxyType:Ext.data.HttpProxy,doSave:function(B,D){if(typeof B=="undefined"){throw new Error("No instance provided to REST Adapter save")}D=D||{};var A=D.success||Ext.emptyFn,C=D.failure||Ext.emptyFn;delete D.success;delete D.failure;Ext.Ajax.request(Ext.apply({url:this.instanceUrl(B),method:B.newRecord()?this.createMethod:this.updateMethod,params:this.buildPostData(B),success:function(E,F,G){G=G||this;return function(H,I){var L=E.modelName.underscore(),K=Ext.decode(H.responseText)[L];for(var J in K){E.set(J,K[J])}F.call(this,E)}}(B,A,D.scope)},D))},afterSave:function(){},doFind:function(E,B,C){E=E||{};B=B||{};var F=(E.primaryKey!==undefined),A=B.url||this.findUrl(E,C);Ext.applyIf(B,{conditions:E,scope:this});var D=F?this.doSingleFind:this.doCollectionFind;return D.call(this,A,B,C)},doDestroy:function(B,C,D){var C=C||{};if(typeof B=="undefined"){throw new Error("No instance provided to REST Adapter destroy")}if(!(B instanceof Ext.data.Record)){var E=parseInt(B,10);B=new D();B.set(D.prototype.primaryKey,E)}var A=C.success||Ext.emptyFn;delete C.success;return Ext.Ajax.request(Ext.applyIf(C,{method:this.destroyMethod,url:this.instanceUrl(B),success:function(){A.call(C.scope||this,B)}}))},doSingleFind:function(D,C,F){var H=C.callback,A=C.success,G=C.failure;delete C.callback;delete C.success;delete C.failure;var E=this.decodeSingleLoadResponse;var B=function(J,I){if(typeof J=="function"){J.apply(C.scope,I)}};Ext.Ajax.request(Ext.apply(C,{callback:function(K,L,J){if(L===true){var I=new F(E(J.responseText,F));B(A,[I,K,J])}else{B(G,arguments)}B(H,arguments)}},this.buildProxyConfig(D)))},storeConfig:{autoLoad:true,remoteSort:false},doCollectionFind:function(B,A,C){Ext.applyIf(A,this.storeConfig);if(A.conditions!=undefined){Ext.applyIf(A,{baseParams:A.conditions})}return new Ext.data.Store(Ext.applyIf(A,{reader:C.prototype.getReader(),proxy:new this.proxyType(this.buildProxyConfig(B))}))},instanceUrl:function(A){if(A.newRecord()){return String.format("/{0}",A.tableName)}else{return String.format("/{0}/{1}",A.tableName,A.get(A.primaryKey))}},collectionUrl:function(A){return String.format("/{0}",A.prototype.tableName)},buildProxyConfig:function(A){return{url:A,method:this.readMethod}},buildPostData:function(A){var C={},B=A.modelName.underscore();for(key in A.data){C[B+"["+key+"]"]=A.data[key]}return C},decodeSingleLoadResponse:function(C,A){var B=A.prototype.tableName;return Ext.decode(C)[B]},findUrl:function(C,B){if(typeof(C)=="object"&&C.primaryKey){var A=new B({});A.set(A.primaryKey,C.primaryKey);delete C.primaryKey;return this.instanceUrl(A)}else{return this.collectionUrl(B)}}});ExtMVC.model.plugin.adapter.RESTJSONAdapter=Ext.extend(ExtMVC.model.plugin.adapter.RESTAdapter,{doSave:function(A,B){if(typeof A=="undefined"){throw new Error("No instance provided to REST Adapter save")}Ext.applyIf(B||{},{jsonData:A.data,params:{},headers:{"Content-Type":"application/json"}});ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.doSave.apply(this,arguments)},doDestroy:function(A,B,C){B=B||{};Ext.applyIf(B,{headers:{"Content-type":"application/json"}});ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.doDestroy.call(this,A,B,C)},decodeSingleLoadResponse:function(C,A){var B=ExtMVC.Inflector.singularize(A.prototype.tableName);return Ext.decode(C)[B]},buildProxyConfig:function(A){var B=ExtMVC.model.plugin.adapter.RESTJSONAdapter.superclass.buildProxyConfig.apply(this,arguments);return Ext.apply(B,{headers:{"Content-Type":"application/json"}})}});Ext.ns("ExtMVC.model.plugin.validation");ExtMVC.model.plugin.validation.AbstractValidation=function(A,C,B){this.ownerClass=A;this.field=C;Ext.apply(this,B)};ExtMVC.model.plugin.validation.AbstractValidation.prototype={getValue:function(A){return A.get(this.field)},isValid:function(A){return true}};ExtMVC.model.plugin.validation.ValidatesPresenceOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{message:"must be present",isValid:function(A){var C=this.getValue(A),B=false;switch(typeof C){case"object":if(C!=null){B=true}break;case"string":if(C.length!=0){B=true}break}return B}});ExtMVC.model.plugin.validation.ValidatesLengthOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{tooShortMessage:"is too short",tooLongMessage:"is too long",message:"",isValid:function(A){var B=this.getValue(A);if(typeof B=="undefined"){return true}if(this.minimum&&B.lengththis.maximum){this.message=this.tooLongMessage;return false}return true}});ExtMVC.model.plugin.validation.ValidatesNumericalityOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{message:"must be a number",isValid:function(A){return"number"==typeof this.getValue(A)}});ExtMVC.model.plugin.validation.ValidatesInclusionOf=Ext.extend(ExtMVC.model.plugin.validation.AbstractValidation,{constructor:function(A,C,B){B=B||{};Ext.applyIf(B,{allowed:[]});ExtMVC.model.plugin.validation.ValidatesInclusionOf.superclass.constructor.call(this,A,C,B);Ext.applyIf(this,{message:"must be one of "+this.allowed.toSentence("or")})},isValid:function(A){var C=this.getValue(A);for(var B=0;B-1){E.push(this.buildColumn(F.name))}},this);Ext.each(A,function(F){if(this.preferredColumns.indexOf(F.name)==-1&&this.ignoreColumns.indexOf(F.name)==-1){E.push(this.buildColumn(F.name))}if(this.wideColumns.indexOf(F.name)){wideColumns.push(F.name)}},this);for(var D=E.length-1;D>=0;D--){var B=E[D];if(this.narrowColumns.indexOf(B.id)>-1){Ext.applyIf(B,{width:this.narrowColumnWidth})}else{if(this.wideColumns.indexOf(B.id)>-1){Ext.applyIf(B,{width:this.wideColumnWidth})}else{Ext.applyIf(B,{width:this.normalColumnWidth})}}}}return E},getFields:function(){if(this.useColumns===undefined){return this.model.prototype.fields.items}else{var A=[];Ext.each(this.useColumns,function(B){A.push({name:B})},this);return A}},buildColumn:function(A){var A=A||{};if(typeof(A)=="string"){A={name:A}}return Ext.applyIf(A,{id:A.name,header:A.name.replace(/_/g," ").titleize(),sortable:true,dataIndex:A.name})},hasAddButton:true,hasEditButton:true,hasDeleteButton:true,buildAddButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"New "+this.model.prototype.singularHumanName,scope:this,iconCls:"add",handler:this.onAdd}))},buildEditButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"Edit selected",scope:this,iconCls:"edit",disabled:true,handler:this.onEdit}))},buildDeleteButton:function(A){return new Ext.Button(Ext.applyIf(A||{},{text:"Delete selected",disabled:true,scope:this,iconCls:"delete",handler:this.onDelete}))},buildTopToolbar:function(){var A=[];if(this.hasAddButton===true){this.addButton=this.buildAddButton();A.push(this.addButton,"-")}if(this.hasEditButton===true){this.editButton=this.buildEditButton();A.push(this.editButton,"-")}if(this.hasDeleteButton===true){this.deleteButton=this.buildDeleteButton();A.push(this.deleteButton,"-")}if(this.hasSearchField===true){this.searchField=this.buildSearchField();A.push(this.searchField,"-")}this.getSelectionModel().on("selectionchange",function(B){if(B.getCount()>0){if(this.deleteButton!=undefined){this.deleteButton.enable()}if(this.editButton!=undefined){this.editButton.enable()}}else{if(this.deleteButton!=undefined){this.deleteButton.disable()}if(this.editButton!=undefined){this.editButton.disable()}}},this);return A},pageSize:25,buildBottomToolbar:function(A){return new Ext.PagingToolbar({store:A,displayInfo:true,pageSize:this.pageSize,emptyMsg:String.format("No {0} to display",this.model.prototype.pluralHumanName)})},hasSearchField:false,searchParamName:"q",buildSearchField:function(){this.searchField=new Ext.form.TwinTriggerField({width:200,validationEvent:false,validateOnBlur:false,hideTrigger1:true,onTrigger1Click:this.clearSearchField.createDelegate(this,[]),onTrigger2Click:this.onSearch.createDelegate(this,[]),trigger1Class:"x-form-clear-trigger",trigger2Class:"x-form-search-trigger"});this.searchField.on("specialkey",function(B,A){if(A.getKey()===A.ESC){this.clearSearchField()}A.stopEvent();if(A.getKey()===A.ENTER){this.onSearch()}},this);return this.searchField},clearSearchField:function(){var A=this.searchField;A.el.dom.value="";A.triggers[0].hide();this.doSearch()},onSearch:function(){var B=this.searchField,A=B.getRawValue();if(A.length<1){this.clearSearchField()}else{B.triggers[0].show();this.doSearch(A)}},doSearch:function(A){A=A||this.searchField.getRawValue()||"";var B={start:0};this.store.baseParams=this.store.baseParams||{};this.store.baseParams[this.searchParamName]=A;this.store.reload({params:B})},onAdd:function(){this.fireEvent("new")},onEdit:function(B){var A=this.getSelectionModel().getSelected();if(A){this.fireEvent("edit",A)}},onDelete:function(){Ext.Msg.confirm("Are you sure?",String.format("Are you sure you want to delete this {0}? This cannot be undone.",this.model.prototype.modelName.titleize()),function(A){if(A=="yes"){var B=this.getSelectionModel().getSelected();if(B){this.fireEvent("delete",B)}}},this)}});Ext.reg("scaffold_index",ExtMVC.view.scaffold.Index);ExtMVC.view.scaffold.New=Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel,{initComponent:function(){Ext.applyIf(this,{title:"New "+this.model.prototype.singularHumanName});ExtMVC.view.scaffold.New.superclass.initComponent.apply(this,arguments)}});Ext.reg("scaffold_new",ExtMVC.view.scaffold.New);ExtMVC.view.scaffold.Edit=Ext.extend(ExtMVC.view.scaffold.ScaffoldFormPanel,{initComponent:function(){Ext.applyIf(this,{title:"Edit "+this.model.prototype.singularHumanName});ExtMVC.view.scaffold.Edit.superclass.initComponent.apply(this,arguments)},loadRecord:function(A){this.instance=A;this.getForm().loadRecord(A)},onSave:function(){this.fireEvent("save",this.instance,this.getFormValues(),this)}});Ext.reg("scaffold_edit",ExtMVC.view.scaffold.Edit);ExtMVC.view.HasManyEditorGridPanel=Ext.extend(Ext.grid.EditorGridPanel,{initComponent:function(){Ext.applyIf(this,{autoScroll:true,store:this.association.findAll(),viewConfig:{forceFit:true}});if(this.hasTopToolbar){this.addTopToolbar()}ExtMVC.view.HasManyEditorGridPanel.superclass.initComponent.apply(this,arguments);this.on("afteredit",function(A){A.record.save({success:function(){A.record.commit()}})},this);this.getSelectionModel().on("selectionchange",function(A,B){if(this.deleteButton){this.deleteButton.enable()}},this)},hasTopToolbar:true,hasNewButton:true,hasDeleteButton:true,addTopToolbar:function(B){var A=[];if(this.hasNewButton){this.newButton=new Ext.Toolbar.Button({iconCls:"add",text:"Add",scope:this,handler:this.onAdd});A.push(this.newButton);A.push("-")}if(this.hasDeleteButton){this.deleteButton=new Ext.Toolbar.Button({text:"Remove selected",disabled:true,iconCls:"delete",scope:this,handler:this.onDelete});A.push(this.deleteButton)}Ext.applyIf(this,{tbar:A})},windowConfig:{},onAdd:function(A){if(!this.addWindow){this.addWindow=new Ext.Window(Ext.applyIf(this.windowConfig,{title:"New",layout:"fit",modal:true,height:300,width:400,items:[this.form],closeAction:"hide",buttons:[{text:"Save",iconCls:"save",scope:this,handler:this.onSaveNew},{text:"Cancel",iconCls:"cancel",scope:this,handler:this.onCancelNew}]}))}this.addWindow.show()},onDelete:function(B){var A=this.getSelectionModel().selection.record;if(A){A.destroy({scope:this,success:function(){this.store.reload()},failure:function(){Ext.Msg.alert("Delete failed","Something went wrong while trying to delete - please try again");this.store.reload()}})}this.deleteButton.disable()},onSaveNew:function(){this.association.create(this.form.getForm().getValues(),{scope:this,success:function(B,A){this.store.reload();this.addWindow.hide()},failure:function(B,A){this.form.getForm().clearInvalid();this.form.getForm().markInvalid(B.errors.forForm())}})},onCancelNew:function(A){this.addWindow.hide()}});Ext.reg("hasmany_editorgrid",ExtMVC.view.HasManyEditorGridPanel);ExtMVC.view.FormWindow=Ext.extend(Ext.Window,{modal:true,height:230,width:400,initComponent:function(){this.form=this.buildForm();Ext.apply(this,{items:[this.form],buttons:[{text:"Save",iconCls:"save",scope:this,handler:this.onSave},{text:"Cancel",iconCls:"cancel",scope:this,handler:this.onCancel}],layout:"fit",closeAction:"hide"});ExtMVC.view.FormWindow.superclass.initComponent.apply(this,arguments)},buildForm:function(){return new Ext.form.FormPanel({})},loadRecord:function(A){this.instance=A;this.form.form.loadRecord(A)},onSave:function(){this.fireEvent("save",this.getFormValues(),this)},onCancel:function(){this.hide()},getFormValues:function(){var B=this.form.getForm(),A={};B.items.each(function(D){var C=(typeof D.getSubmitValue=="function")?"getSubmitValue":"getValue";A[D.getName()]=D[C]()},this);return A}});Ext.reg("formwindow",ExtMVC.view.FormWindow); \ No newline at end of file diff --git a/ext-mvc-all.js b/ext-mvc-all.js index 9280ca1..3e88119 100644 --- a/ext-mvc-all.js +++ b/ext-mvc-all.js @@ -46,6 +46,151 @@ ExtMVC = Ext.extend(Ext.util.Observable, { this.name = this.app.name; }, + /** + * @property bootParams + * @type Object + * An object which contains all boot parameters. These are used during the boot phase, + * and can be set using GET params after the '?' in the url + */ + bootParams: { + environment: 'production' + }, + + /** + * @property globalEnvironmentSettings + * @type Object + * All default environment settings that will be Ext.applyIf'd to the current environment. + * These are things that don't tend to change between applications, but you can override them if you need to + */ + globalEnvironmentSettings: { + pluginsDir : '../vendor/plugins', + libDir : '../lib', + configDir : '../config', + overridesDir: '../config/overrides', + appDir : '../app', + vendor : ['mvc'], + mvcFilename : 'ext-mvc-all-min', + config : ['initialize', 'database', 'routes'] + }, + + /** + * Boots up the application. + * TODO: When it works, document this :) + */ + boot: function() { + var args = window.location.href.split("?")[1]; + + /** + * Read config data from url parameters + */ + if (args != undefined) { + Ext.each(args.split("&"), function(arg) { + var splits = arg.split("="), + key = splits[0], + value = splits[1]; + + this.bootParams[key] = value; + }, this); + } + + + //load up the environment + Ext.Ajax.request({ + url: '../config/environment.json', + scope : this, + success: function(response, options) { + var envName = this.bootParams.environment; + + this.addEnvironmentSettings(envName, this.globalEnvironmentSettings); + this.addSettingsFromEnvironmentFile(response); + this.setCurrentEnvironment(envName); + + Ext.Ajax.request({ + url : String.format("../config/environments/{0}.json", envName), + success: function(response, options) { + this.addSettingsFromEnvironmentFile(response); + + this.onEnvironmentLoaded(this.getCurrentEnvironmentSettings()); + }, + scope : this + }); + }, + failure: function() { + Ext.Msg.alert( + 'Could not load environment', + 'The ' + config.environment + ' environment could not be found' + ); + } + }); + }, + + /** + * Called when the environment files have been loaded and application load can begin + * @param {Object} environment The current environment object + */ + onEnvironmentLoaded: function(env) { + var order = ['overrides', 'config', 'plugins', 'models', 'controllers', 'views'], + files = []; + + // Ext.each(order, function(fileList) { + // var dir = env[fileList + "Dir"] || String.format("../app/{0}", fileList); + // + // Ext.each(env[fileList], function(file) { + // files.push(String.format("{0}/{1}.js", dir, file)); + // }, this); + // }, this); + + Ext.each(env.overrides, function(file) { + files.push(String.format("{0}/{1}.js", env.overridesDir, file)); + }, this); + + Ext.each(env.config, function(file) { + files.push(String.format("{0}/{1}.js", env.configDir, file)); + }, this); + + Ext.each(env.plugins, function(file) { + files.push(String.format("{0}/{1}/{2}-all.js", env.pluginsDir, file, file)); + }, this); + + Ext.each(env.models, function(file) { + files.push(String.format("{0}/models/{1}.js", env.appDir, file)); + }, this); + + Ext.each(env.controllers, function(file) { + files.push(String.format("{0}/controllers/{1}Controller.js", env.appDir, file)); + }, this); + + Ext.iterate(env.views, function(dir, fileList) { + Ext.each(fileList, function(file) { + files.push(String.format("{0}/views/{1}/{2}.js", env.appDir, dir, file)); + }, this); + }, this); + + var fragment = ""; + for (var i=0; i < files.length; i++) { + fragment += String.format("", files[i]); + }; + + Ext.getBody().createChild(fragment); + + // Ext.onReady(function() { + // console.log('hmm'); + // console.log('test'); + // console.log(this); + // this.app.onReady(); + // }, this); + }, + + + /** + * @private + * Takes the response of an AJAX request, encodes it into a JSON object and adds to the current environment + */ + addSettingsFromEnvironmentFile: function(response) { + var envJSON = Ext.decode(response.responseText); + this.addEnvironmentSettings(this.bootParams.environment, envJSON); + }, + /** * @property controllers * When this.registerController('application', MyApp.ApplicationController) is called, @@ -174,34 +319,40 @@ ExtMVC.App = Ext.extend(Ext.util.Observable, { this.initializeNamespaces(); - Ext.onReady(function() { - if (this.fireEvent('before-launch', this)) { - this.initializeRouter(); - // this.initializeViewport(); - this.initializeEvents(); + Ext.onReady(this.onReady, this); + }, + + /** + * @private + * Called when Ext.onReadt fires + */ + onReady: function() { + if (this.fireEvent('before-launch', this)) { + this.initializeRouter(); + // this.initializeViewport(); + this.initializeEvents(); - if (this.usesHistory === true) this.initializeHistory(); + if (this.usesHistory === true) this.initializeHistory(); - this.launch(); - this.fireEvent('launched', this); - - /** - * TODO: This used to reside in initializeHistory but this.launch() needs to be - * called before this dispatches so it is temporarily here... ugly though - */ - if (this.usesHistory) { - if (this.dispatchHistoryOnLoad === true) { - Ext.History.init(function(history) { - var hash = document.location.hash.replace("#", ""); - var params = this.router.recognise(hash); - if (params) {this.dispatch(params);} - }, this); - } else { - Ext.History.init(); - } + this.launch(); + this.fireEvent('launched', this); + + /** + * TODO: This used to reside in initializeHistory but this.launch() needs to be + * called before this dispatches so it is temporarily here... ugly though + */ + if (this.usesHistory) { + if (this.dispatchHistoryOnLoad === true) { + Ext.History.init(function(history) { + var hash = document.location.hash.replace("#", ""); + var params = this.router.recognise(hash); + if (params) {this.dispatch(params);} + }, this); + } else { + Ext.History.init(); } } - }, this); + } }, /** @@ -1485,12 +1636,16 @@ ExtMVC.controller.CrudController = Ext.extend(ExtMVC.controller.Controller, { * Renders the custom New view if present, otherwise falls back to the default scaffold New form */ build: function() { - return this.render('New', { + var buildView = this.render('New', { model : this.model, controller : this, listeners : this.getBuildViewListeners(), viewsPackage: this.viewsPackage }); + + this.onBuild(buildView); + + return buildView; }, /** @@ -1657,6 +1812,13 @@ ExtMVC.controller.CrudController = Ext.extend(ExtMVC.controller.Controller, { this.fireEvent('delete-failed', instance); }, + /** + * Called whenever the 'New' form has been rendered for a given instance. This is an empty function by default, + * which you can override to provide your own logic if needed + * @param {Ext.Component} form The rendered 'New' form + */ + onBuild: function(form) {}, + /** * Called whenever the Edit form has been rendered for a given instance. This is an empty function by default, * which you can override to provide your own logic if needed diff --git a/scripts/plugin.rb b/scripts/plugin.rb index 8947508..70dc416 100644 --- a/scripts/plugin.rb +++ b/scripts/plugin.rb @@ -1,70 +1,156 @@ require 'ftools' module ExtMVC - class Plugin - attr_reader :name, :type, :directory - - def initialize name - @name = @directory = name - @type = @name =~ /git:\/\// ? 'git' : 'dir' + module Plugin + def self.dispatch + meth = ARGV.shift.downcase + name = ARGV.shift - @public_directory = "#{@name}/public" - end - - def install - install_assets + self.send(meth, name) end - def install_assets - install_assets_from_directory(@public_directory) if File.exists?(@public_directory) + def self.build(name) + return self.build_all if name == 'all' + + dirName = "vendor/plugins/#{name}" + pluginFileName = "#{dirName}/#{name}-all.js" + buildFiles = [] + + if File.exists?("#{dirName}/include_order.txt") + #Load using the include file + includeFile = File.open("#{dirName}/include_order.txt", "r") + while (line = includeFile.gets) + line.gsub!(/\n/, '') + next if line =~ /^#/ || line.length.zero? + buildFiles.push("#{dirName}/#{line}") + end + else + #Load all files in the order they are found + Dir["#{dirName}/**/*.js"].each do |fileName| + next if fileName =~ /-all.js$/ + buildFiles.push(fileName) + end + end + + self.concatenate_files(buildFiles, pluginFileName) + puts end - def uninstall - if File.exists?(@public_directory) - + def self.build_all + dirs = [] + pluginsPath = 'vendor/plugins' + + Dir.entries(pluginsPath).each do |fileName| + dirs.push(fileName) if File.directory?("#{pluginsPath}/#{fileName}") && (fileName =~ /^\./) != 0 end + + dirs.each {|plugin| self.build(plugin)} end - private - #installs all assets from the given directory and subdirs into the main /public folder - def install_assets_from_directory(directory) - directory ||= @public_directory - - find_assets_in_directory(directory).each do |f| - new_asset = asset_new_location_name(f) - puts new_asset - if File.directory?(f) - Dir.mkdir(new_asset) unless File.exists?(new_asset) - puts "Created directory: " + new_asset - else - File.copy(f, new_asset) - end - end + # Installs a given plugin by copying its public assets + def self.install(name) + end - #recursively finds assets in directories under /public inside the plugin - def find_assets_in_directory(directory) - files = [] + # Uninstalls a given plugin by removing its public assets + def self.uninstall(name) - Dir.entries(directory).each do |e| - filename = File.join(directory, e) - next if ['.', '..'].include?(filename.split('/').last.to_s) - - files.push(filename) - files.concat(find_assets_in_directory(filename)) if File.directory?(filename) - end + end + + # Creates a new skeleton plugin + def self.create(name) - return files end - #i.e. vendor/plugins/MyPlugin/public/images/image.gif becomes public/images/image.gif - def asset_new_location_name(filename) - pieces = filename.split("/") - if index = pieces.index('public') - File.join(pieces.slice(index, pieces.size)) - else - filename + private + # TODO: this is copied verbatim from the builder script. Refactor this and builder's version somewhere else + def self.concatenate_files(files, concatenated_filename, baseDir = './') + #remove old files, create blank ones again + File.delete(concatenated_filename) and puts "Deleted old #{concatenated_filename}" if File.exists?(concatenated_filename) + FileUtils.touch(concatenated_filename) + + count = 0 + file = File.open(concatenated_filename, 'w') do |f| + files.each do |i| + # remove the directory the app is in if add_dir is supplied + i = i.gsub(Regexp.new(ENV['app_dir']), '').gsub(/$(\/*)(.*)/, '\2') if ENV['app_dir'] + + f.puts(IO.read(File.join(baseDir, i))) + f.puts("\n") + count += 1 + end end + + puts "Concatenated #{count} files into #{concatenated_filename}" end end -end \ No newline at end of file +end + +# module ExtMVC +# class Plugin +# attr_reader :name, :type, :directory +# +# def initialize name +# @name = @directory = name +# @type = @name =~ /git:\/\// ? 'git' : 'dir' +# +# @public_directory = "#{@name}/public" +# end +# +# def install +# install_assets +# end +# +# def install_assets +# install_assets_from_directory(@public_directory) if File.exists?(@public_directory) +# end +# +# def uninstall +# if File.exists?(@public_directory) +# +# end +# end +# +# private +# #installs all assets from the given directory and subdirs into the main /public folder +# def install_assets_from_directory(directory) +# directory ||= @public_directory +# +# find_assets_in_directory(directory).each do |f| +# new_asset = asset_new_location_name(f) +# puts new_asset +# if File.directory?(f) +# Dir.mkdir(new_asset) unless File.exists?(new_asset) +# puts "Created directory: " + new_asset +# else +# File.copy(f, new_asset) +# end +# end +# end +# +# #recursively finds assets in directories under /public inside the plugin +# def find_assets_in_directory(directory) +# files = [] +# +# Dir.entries(directory).each do |e| +# filename = File.join(directory, e) +# next if ['.', '..'].include?(filename.split('/').last.to_s) +# +# files.push(filename) +# files.concat(find_assets_in_directory(filename)) if File.directory?(filename) +# end +# +# return files +# end +# +# #i.e. vendor/plugins/MyPlugin/public/images/image.gif becomes public/images/image.gif +# def asset_new_location_name(filename) +# pieces = filename.split("/") +# if index = pieces.index('public') +# File.join(pieces.slice(index, pieces.size)) +# else +# filename +# end +# end +# end +# end \ No newline at end of file diff --git a/scripts/templates/Model.js b/scripts/templates/Model.js index 8565fbe..93758b3 100644 --- a/scripts/templates/Model.js +++ b/scripts/templates/Model.js @@ -1,8 +1,8 @@ /** * @class <%= @name %> - * @extends ExtMVC.Model + * @extends ExtMVC.model.Base */ -ExtMVC.Model.define("<%= @name %>", { +ExtMVC.model.define("<%= @name %>", { fields: [ <%= @fields %> ]