PDA

View Full Version : [3.0] Ext.Direct Routers for Rails and Merb



christocracy
1 May 2009, 4:04 PM
I released a beta version of the Ext.Direct router for Rails yesterday.
http://rubyforge.org/projects/rails-extjs/

See README.rdoc where I wrote a 3-step setup process.


>sudo gem install rails-extjs-direct


This gem is very new and I have yet to handle file-uploads from multipart forms but that won't take more than 2 hours. The Rails router is implemented using Rack Middle Ware (http://railscasts.com/episodes/151-rack-middleware) so you need Rails 2.3.2+. The Rails gem will route transactions to multiple ApplicationControllers during a single Ajax request.

The Merb gem has been around a little longer.
http://rubyforge.org/projects/merb-extjs/



>sudo gem install merb-extjs-direct


The Merb gem is implemented with merb-parts (http://yehudakatz.com/2007/09/06/merbs-parts-are-pretty-rocking-too/) so Ext.Direct requests will be routed to actions within your Merb::PartControllers. I'm looking forward to something like merb-parts (http://yehudakatz.com/2007/09/06/merbs-parts-are-pretty-rocking-too/) in Rails3.

Both the Rails and Merb gem have a very small code footprint, there's really not much to them. If anyone wishes to contribute to either of these projects, let me know and I'll provide access to the repos on Rubyforge.

I foresee adding more gems to these namespaces "merb-extjs" and "rails-extjs" to assist with Rails/Merb development with Ext JS.

Please let me know of any issues or suggestions.

edspencer
7 May 2009, 8:50 AM
Thanks for putting this up.

I'm a Rails guy too and have been looking through this - what I'm finding difficult at the moment is getting any 'editorial' kind of info about what Writers and Direct actually do... is there anything I can look at about this?

Poking around at the gem code I can see how it's looping through a bunch of 'requests' inside the single request received, but how do you provide error feedback this way? If I want to execute a few actions and they're bundled into a single Direct request, how do I regain control if one of them fails? What do I do if my save doesn't pass validation, or my load hits a 404?

It looks like REST is out of the window with this approach too - do you think that makes it harder for other applications to use the same server API?

I hope I don't sound negative... I just don't understand how it's meant to work at the moment!

christocracy
7 May 2009, 3:47 PM
Poking around at the gem code I can see how it's looping through a bunch of 'requests' inside the single request received, but how do you provide error feedback this way?

In SVN for that gem, I've added a rescue_action to the controller mixin Rails::ExtJS::Direct::Controller. The rescue_action is looking for exceptions of class XException. XException is provided by the rails-extjs-direct gem. Notice how it simply returns an Ajax response of type XExceptionResponse. Ext.Direct is prepared to receive this type of response (simply a json response having "type":"exception")



def rescue_action(e)
if (e.kind_of?(XException))
render :json => XExceptionResponse.new(@xrequest, e)
else
raise e
end
end


Raising an XException in a controller action:



class UsersController < ApplicationController
include Rails::ExtJS::Direct::Controller

def create
raise XException.new("A create exception!!!")
end
end

ttsuchi
16 May 2009, 5:57 PM
Thanks for the gem! I'm starting to take a look at the gem today. One thing I noticed it is that it doesn't support namespaced controllers yet... (Well, I'm sure there're lots of things that's not supported yet, but this was the first roadblock I encountered.) I want to use it under my "/admin/*" URL, and so want to invoke Admin::SomeController instead.

So looking at the gem source, I made the following changes:



def initialize(app, rpath)
...
@ns = rpath[0...rpath.rindex('/')]
end

def call(env)
...
request_env["PATH_INFO"] = "#{@ns}/#{controller}/#{action}"
request_env["REQUEST_URI"] = "#{@ns}/#{controller}/#{action}"
...
end
and in my environment.rb



config.middleware.use Rails::ExtJS::Direct::RemotingProvider, "/admin/direct"
So basically, when "/admin/direct" router is defined, this would try to forward reqs to "/admin/*" URL. This is not a great solution - this way, the app can handle only one namespace. Maybe a better approach is to pass the namespace param from Ext.Direct (like baseParams: {ns: '/admin'}) and get that value from there, but I haven't looked into the Ext.Direct.addProvider yet to know if that's possible :-?

ttsuchi
17 May 2009, 2:44 PM
Also, maybe the default of the @params in XRequest should be a Hash ({}) as opposed to array ([])?

chriss
20 May 2009, 12:41 AM
You can solve this problem by modifying the remoting_provider.rb file from the gem( ruby\gems\1.8\gems\rails-extjs-direct-0.0.3\lib\rails-extjs-direct\rack).
Replace

controller = req.delete("action")with

controller = req.delete("action").gsub("::","/")
The only disadvantage is that you'll have to call your provider by
window[Namespace::Controller]

somebee
20 May 2009, 1:46 AM
I have created a merb-router myself, which does not use Parts at all. It exposes all the public controllers and actions automatically (has a helper for generating the direct-api javascript), and maps to the exact same controllers as your public api etc.

This is done with merb-action-args, so that that I can do this:



class Topics < Application
direct :expose => :all

def show(id)
@topic = Topic.get(id)
display @topic
end

end

When you include the js-helper, you can automagically call:

Topics.show(10, function(topic){ /*callback*/}) in your client-code.

But the same action can also be accessed via regular http, like:
http://yourapp/topics/10.json => {:id => 10, :title => "Hello", ...}
Or without resource-routes:
http://yourapp/topics/show.json?id=10 => {:id => 10, :title => "Hello", ...}

So, with merb-action-args there is no need to have duplicate code.
The only thing you need to set it up is to add this to router.rb:

Merb::Router.prepare do

direct("/rpc.json") # or the url you want to use

...

end



I'm planning on releasing it on github, but the code itself is still a little messy, so need to do some cleaning first :-)

plaak
3 Jun 2009, 9:50 AM
Chris,

Thanks for the rails code! This seems a much cleaner approach han trying to directly request rails' RESTful URLs and deal with ajax requests in multiple places.

A few questions/coments though:

1) It appears to completely by-pass rails authenticity token step. This means that any malicious individual could directly access your /direct path and pass json encoded parameters onto the controller.... thereby facilitating CSRF attacks. I would think it would be pretty simple to pass an authenticity_token property into the provider when it's instantiated and modify your rack code to make it required (perhaps as an option)...

2) It seems that the javascript "model" name must exactly match the name of the controller. In other words, I'd like to use the singular form of each recordset like I do with my rails' models, but most of my controllers are plural. It'd be neat if I could use either and the middleware resolve which, i.e., if the singular form of the controller does not exist, check for the plural one...

Maybe when I have time, I'll make the changes myself....

Thanks again,
Pete

christocracy
3 Jun 2009, 9:56 AM
Would you like SVN access to the gem?

plaak
3 Jun 2009, 10:03 AM
Sure, I can't make any promises though :-)

christocracy
3 Jun 2009, 10:37 AM
PM me with your email address.

christocracy
3 Jun 2009, 10:39 AM
I have created a merb-router myself, which does not use Parts at all. It exposes all the public controllers and actions automatically (has a helper for generating the direct-api javascript), and maps to the exact same controllers as your public api etc.

This is done with merb-action-args, so that that I can do this:


Nice!

cbourne
9 Jun 2009, 10:19 AM
Hi this is very intersting.

Is there any chance of providing a simple Sinatra example of this working?

Since Sinatra is Rack based and very light it's great for building basic demos/prototypes. Combining it with Ext.Direct would be superb!

Regards,

Carl

Neville Burnell
28 Jul 2009, 4:45 AM
I'd be interested in seeing Ext.Direct for Sinatra also

christocracy
25 Aug 2009, 4:52 PM
If anyone wishes to add Rails' authenticity_token to each request, here's one way to do it:



Ext.override(Ext.direct.RemotingProvider, {
getCallData: function(t){
return {
action: t.action,
method: t.method,
data: t.data,
type: 'rpc',
tid: t.tid,
authenticity_token: '<%= form_authenticity_token %>'
};
}
});

edspencer
25 Aug 2009, 5:26 PM
If anyone wishes to add Rails' authenticity_token to each request, here's one way to do it:



Ext.override(Ext.direct.RemotingProvider, {
getCallData: function(t){
return {
action: t.action,
method: t.method,
data: t.data,
type: 'rpc',
tid: t.tid,
authenticity_token: '<%= form_authenticity_token %>'
};
}
});


Or, you could monkey patch (http://edspencer.net/2009/07/extoverride-monkey-patching-ext-js.html) it :)



(function() {
var originalGetCallData = Ext.direct.RemotingProvider.prototype.getCallData;

Ext.override(Ext.direct.RemotingProvider, {
getCallData: function(t) {
var defaults = originalGetCallData.apply(this, arguments);

return Ext.apply(defaults, {
authenticity_token: '<%= form_authenticity_token %>'
});
}
})
})();
The main reason for doing it this way is the override won't break if some important property is added or removed from the returned object in the original getCallData function, we just decorate it instead.

I find this pattern coming up most times I override... perhaps we should have a build in function for it - something like:



Ext.decorate(Ext.direct.RemotingProvider, 'getCallData', {
authenticity_token: '<%= form_authenticity_token %>'
});
Which does the same as my first example, and is powered by:



/**
* @param {Function} klass The constructor function of the class to override (e.g. Ext.direct.RemotingProvider)
* @param {String} property The name of the property the function to override is tied to on the klass' prototype
* @param {Object} config An object that is Ext.apply'd to the usual return value of the function before returning
*/
Ext.decorate = function(klass, property, config) {
var original = klass.prototype[property];
override = {};

override[property] = function() {
var value = original.apply(this, arguments);

return Ext.apply(value, config);
};

Ext.override(klass, override);
}
One could probably use Ext's Function methods to do the same job.

christocracy
25 Aug 2009, 5:33 PM
Very nice!

christocracy
25 Aug 2009, 9:08 PM
I added some new code-generation features to rails-extjs-direct gem tonight, version 0.0.5



>sudo gem install rails-extjs-direct


1. Define your direct actions in the controller after including the Mixin:


class ExamplesController < ApplicationController
include Rails::ExtJS::Direct::Controller

direct_actions :foo, :bar
.
.
.
end


2. Get a RemotingProvider instance with new helper method get_extjs_direct_provider. Let your partials add actions to the @provider instance variable.
index.html.erb


<h1>Ext.Direct Remoting Provider Helpers</h1>

<% @provider = get_extjs_direct_provider('remoting', '/direct') %>

<%= render(:partial => "foo") %>

<script>
<%= @provider.render %>
</script>


3. Adding actions to the @provider instance from within a Partial:
_foo.html.erb


<h1>foo partial</h1>

<% @provider.add_controller("examples") %>


5. Rendering the loaded RemotingProvider:


<script>
<%= @provider.render %>
</script>


Will output:


Ext.Direct.addProvider({type: 'remoting', url: '/direct', actions: {"Examples":[{"name":"foo", "len":1},{"name":"bar", "len":1}]}});

Note: All Direct-actions are rendered with an argument length of 1.

DAddYE
15 Sep 2009, 4:27 AM
Hey,

why you don't help us to integrate them in LipsiADMIN (http://lipsiadmin.com) ?

kampnerj
22 Sep 2009, 7:00 AM
rails-extjs-direct seems to be broken on rails releases > 2.3.2

I get a message about a missing template...


Processing TestController#destroy (for 127.0.0.1 at 2009-09-22 10:58:27) [POST]
Parameters: {"tid"=>2, "type"=>"rpc", "data"=>[1]}


Processing ApplicationController#index (for 127.0.0.1 at 2009-09-22 10:58:27) [POST]
Parameters: {"id"=>1}

ActionView::MissingTemplate (Missing template test/destroy.erb in view path app/views):
/usr/lib/ruby/gems/1.8/gems/rails-extjs-direct-0.0.11/lib/rails-extjs-direct/mixins/action_controller/direct_controller.rb:67:in `rescue_action'
/usr/lib/ruby/gems/1.8/gems/rails-extjs-direct-0.0.11/lib/rails-extjs-direct/rack/remoting_provider.rb:46:in `call'
/usr/lib/ruby/gems/1.8/gems/rails-extjs-direct-0.0.11/lib/rails-extjs-direct/rack/remoting_provider.rb:29:in `each'
/usr/lib/ruby/gems/1.8/gems/rails-extjs-direct-0.0.11/lib/rails-extjs-direct/rack/remoting_provider.rb:29:in `call'

Rendering rescues/layout (internal_server_error)


Am I doing something wrong? This works perfectly when I switch the environment back to rails 2.3.2.

quorak
27 Sep 2009, 11:34 PM
Thanks for the gem! I'm starting to take a look at the gem today. One thing I noticed it is that it doesn't support namespaced controllers yet... (Well, I'm sure there're lots of things that's not supported yet, but this was the first roadblock I encountered.) I want to use it under my "/admin/*" URL, and so want to invoke Admin::SomeController instead.

Any chance to make this a feature?
I think this setup is quite likely as extjs is great for admin interfaces!

best

kampnerj
14 Jan 2010, 11:37 AM
Is this going to support the formHandler option at some point soon?

plaak
14 Jan 2010, 10:03 PM
Not quite sure I know why this is happening now...

This gem is causing non-direct requests on actions I am not using with direct but which share a controller to get intercepted somehow and strip the params hash to zero. At first it seemed that setting direct_actions for just those specific direct actions fixed the problem...but apparently not. I have sort of fixed this by directly patching lib/rails-extjs-direct/mixins/action_controller/direct_controller.rb and commenting out the delete call in the params.each loop...

I presume the point of this function of clearing out params on a Direct request is to ensure it is pristine when the values are provided to the controller.... but I'm not quite sure why it's doing this for non-direct requests or if I have something mis-configured somehow???

jarrednicholls
15 Jan 2010, 2:13 PM
Or, you could monkey patch (http://edspencer.net/2009/07/extoverride-monkey-patching-ext-js.html) it :)



(function() {
var originalGetCallData = Ext.direct.RemotingProvider.prototype.getCallData;

Ext.override(Ext.direct.RemotingProvider, {
getCallData: function(t) {
var defaults = originalGetCallData.apply(this, arguments);

return Ext.apply(defaults, {
authenticity_token: '<%= form_authenticity_token %>'
});
}
})
})();
The main reason for doing it this way is the override won't break if some important property is added or removed from the returned object in the original getCallData function, we just decorate it instead.

I find this pattern coming up most times I override... perhaps we should have a build in function for it - something like:



Ext.decorate(Ext.direct.RemotingProvider, 'getCallData', {
authenticity_token: '<%= form_authenticity_token %>'
});
Which does the same as my first example, and is powered by:



/**
* @param {Function} klass The constructor function of the class to override (e.g. Ext.direct.RemotingProvider)
* @param {String} property The name of the property the function to override is tied to on the klass' prototype
* @param {Object} config An object that is Ext.apply'd to the usual return value of the function before returning
*/
Ext.decorate = function(klass, property, config) {
var original = klass.prototype[property];
override = {};

override[property] = function() {
var value = original.apply(this, arguments);

return Ext.apply(value, config);
};

Ext.override(klass, override);
}
One could probably use Ext's Function methods to do the same job.

That is pretty specific. We can create another Ext Function prototype that is similar to createSequence, but will receive the return value from the preceding function and return the result of the sequenced function, and then we can "decorate" accordingly but still have a nice generically usable Ext Function method.

Note, the name is arbitrary :-)



Function.prototype.createFullSequence = function(fcn, scope){
var method = this;
return !Ext.isFunction(fcn) ?
this :
function(){
var retval = method.apply(this || window, arguments);
return fcn.apply(scope || this || window, [retval].concat(arguments));
};
};


We can now decorate:



Ext.override(Ext.direct.RemotingProvider, {
getCallData: Ext.direct.RemotingProvider.prototype.getCallData.createFullSequence(function(defaults){
return Ext.apply(defaults, {
authenticity_token: '<%= form_authenticity_token %>'
});
})
});

jarrednicholls
15 Jan 2010, 2:19 PM
Not quite sure I know why this is happening now...

This gem is causing non-direct requests on actions I am not using with direct but which share a controller to get intercepted somehow and strip the params hash to zero. At first it seemed that setting direct_actions for just those specific direct actions fixed the problem...but apparently not. I have sort of fixed this by directly patching lib/rails-extjs-direct/mixins/action_controller/direct_controller.rb and commenting out the delete call in the params.each loop...

I presume the point of this function of clearing out params on a Direct request is to ensure it is pristine when the values are provided to the controller.... but I'm not quite sure why it's doing this for non-direct requests or if I have something mis-configured somehow???

The mixin module doesn't check to see if the current action for that controller is one specified as a direct_action. The solution to the issue is to put a condition on the :extjs_direct_prepare_request before_filter that makes sure the currently requested action is one located in the extjs_direct_actions collection.

Should be an easy patch. If I didn't have to run now I would patch it real quick myself :-) I'll check back here tonight and if no one's posted a patch then I will do so for sure.

Edit: This makes me wonder (as I'm not an avid Ext.Direct user yet) what happens on Ext.Direct file uploads...is the file located in the "data" parameter, or what? If it's not in the "data" parameter then the file is surely stripped out of the params. I'll be messing with this later, although I wouldn't have ended up using Ext.Direct to post a file anyways :-)

jarrednicholls
15 Jan 2010, 6:47 PM
Ok, here's my untested and unverified patch of direct_controller.rb, so that it first makes sure that the action that's about to run is an extjs_direct_action before preparing the direct request.



--- direct_controller.rb 2010-01-14 23:52:46.733031060 -0500
+++ direct_controller.fixed.rb 2010-01-15 21:39:26.564751681 -0500
@@ -3,7 +3,9 @@
def self.included(base)
base.class_eval do
cattr_accessor :extjs_direct_actions
- before_filter :extjs_direct_prepare_request
+ before_filter do |controller|
+ controller.extjs_direct_prepare_request if controller.extjs_direct_action?
+ end

# include the Helper @see helpers/direct_controller_helper.rb
helper Helper
@@ -18,6 +20,10 @@
end
end

+ def extjs_direct_action?
+ self.extjs_direct_actions.any? { |a| a[:name].to_s == params[:action] }
+ end
+
def extjs_direct_prepare_request
#TODO just populate params with the XRequest data.


Edit: I'd be happy to maintain the rails extjs direct adapter if I were given SVN access, w/ prior approvals of checkins of course. Chris Scott I'll contact you personally, or I'll have Aaron Conran reach you

markmansour
2 Feb 2010, 6:30 PM
I believe the code is available on github.

http://github.com/extjs/direct

You can fork the project, make your own changes, check them back into your own repository, and then the maintainer (Chris) can 'pull' the changes back in.

Mark
--
Mark Mansour
Founder, Agile Bench
http://agilebench.com/

christocracy
2 Feb 2010, 6:37 PM
Anyone interested in Direct for Rails should check out active-direct (http://github.com/stonegao/active-direct).

x1am
4 Feb 2010, 8:14 AM
I've installed rails active-direct plugin, though can't get it running properly.
This is what I get on firebug:
>>> App.models.Customer.all();
POST http://localhost:3000/direct_router 200 OK 17ms jquery...4618017 (5090)
JSON


[{
"result":[{
"customer:{
"name":"test1",
"comment":"",
"created_at":"2010-02-01T07:47:34Z",
"updated_at":"2010-02-04T10:27:31Z",
"id":3
}
},{
"customer":{
"name":"test2...",
"comment":"comment here",
"created_at":"2010-02-03T07:48:38Z",
"updated_at":"2010-02-04T10:27:20Z",
"id":6
}
}],
"method":"all",
"action":"Customer",
"tid":4,
"type":"rpc"
}
]though function returns nothing, and store never gets loaded with following code

<script>
var store = new Ext.data.DirectStore({
storeId:'customers',
api: {read: App.models.Customer.all},
autoLoad: true,
fields: [ 'name','comment']
});
</script>
could anyone please advise on how to use this router correctly?

chriss
6 Feb 2010, 9:27 AM
Any chance to make this a feature?
I think this setup is quite likely as extjs is great for admin interfaces!

best

I had the same problem, and here is what i did

override some ext.direct methods



Ext.override(Ext.direct.RemotingProvider,{


doCall : function(c, m, args){
var data = null, hs = args[m.len], scope = args[m.len+1];

if(m.len !== 0){
data = args.slice(0, m.len);
}

var t = new Ext.Direct.Transaction({
provider: this,
args: args,
action: c,
method: m.name,
namespace: m.namespace,
data: data,
cb: scope && Ext.isFunction(hs) ? hs.createDelegate(scope) : hs
});

if(this.fireEvent('beforecall', this, t) !== false){
Ext.Direct.addTransaction(t);
this.queueTransaction(t);
this.fireEvent('call', this, t);
}
},

doForm : function(c, m, form, callback, scope){
var t = new Ext.Direct.Transaction({
provider: this,
action: c,
method: m.name,
namespace: m.namespace,
args:[form, callback, scope],
cb: scope && Ext.isFunction(callback) ? callback.createDelegate(scope) : callback,
isForm: true
});

if(this.fireEvent('beforecall', this, t) !== false){
Ext.Direct.addTransaction(t);
var isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data',
params = {
extTID: t.tid,
extAction: c,
extMethod: m.name,
extType: 'rpc',
extUpload: String(isUpload)
};

// change made from typeof callback check to callback.params
// to support addl param passing in DirectSubmit EAC 6/2
Ext.apply(t, {
form: Ext.getDom(form),
isUpload: isUpload,
params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params
});
this.fireEvent('call', this, t);
this.processForm(t);
}
},


getCallData: function(t){

console.log(t)
return {
action: t.action,
method: t.method,
data: t.data,
type: 'rpc',
tid: t.tid,
namespace: t.namespace || ''
};
}

})
modify Rails::ExtJS::Direct::RemotingProvider call method, so now contains this



controller = req.delete("action").strip.downcase
action = req.delete("method").strip.downcase
ns = req.delete("namespace").strip.downcase
controller = [ns, controller].join("/") if !ns.blank?
And now i can define my provider like this


Ext.Direct.addProvider({
type:"remoting",
url:"/direct",
actions:{
"main":[{
"name":"load",
"len":1,
'namespace':'ruby_namespace'
}]
},
"namespace":"js_namespace"
})

freedumb2000
24 Jul 2010, 3:02 PM
Is rails-extjs-direct still in active development? Can it be recommended for rails 3b? And most importantly, is there any documentation besides just looking at the source?
If not I might as well write my own adapter and helpers for ext.direct ;)