I first learned about Data, context, and interaction (DCI) through this blog post. Fascinated by the concept, I endeavored to build it in to my next Rails application. Since DCI works in tandem with MVC, I thought it wouldn't be too hard to make the API RESTful at the same time. So I made a RESTful resource, Report
and extend it with various contexts. The way I implemented contexts in Rails was by creating a directory, /app/contexts/
, for modules which extend the controller actions. So my reports_controller.rb
looks like this:
class ReportsController < ApplicationController
before_filter :only => :new do |c|
c.switch_context("submission")
end
# GET /reports
def index
@context.report_list
end
# GET /reports/1
def show
@context.display_report
end
# GET /reports/new
def new
@context.new_report
end
# GET /reports/1/edit
def edit
@context.edit_report
end
# POST /reports
def create
@context.create_report
end
def update
@context.update_report
end
# DELETE /reports/1
def destroy
@context.destroy_report
end
protected
def switch_context(context_name)
session[:context] = context_name
context = session[:context].camelize.constantize
@context ||= self.extend context
end
end
And in the application_controller.rb
I set the context with a before_filter
:
class ApplicationController < ActionController::Base
before_filter :contextualize
protect_from_forgery
protected
# Sets the context of both current_user and self
# by extending with /app/roles/role_name
# and /app/contexts/context_name respectively
def contextualize
# Extend self (ActionController::Base) with context
if session[:context]
context_class = session[:context].camelize.constantize
if current_user.allowed_contexts.include?(context_class)
context_class = current_user.context if context_class == Visiting
else
context_class = Visiting
end
else
context_class = current_user.context
end
@context ||= self.extend context_class
end
end
Notice I extend current_user
with a Role
in addition to the controller context.
Here's how it works:
RegisteredUser
.RegisteredUser
's default context is Search
(as defined in /app/roles/registered_user.rb
).Search
context, the user can only view published reports.Submission
and stored in the current_user
's session./app/contexts/submission.rb
context handles the action.The are several other contexts (review, editorial, etc.) and roles (co-author, editor, etc.).
So far this approach has worked well for the most part. But there is a flaw: when a user opens multiple browser windows and changes contexts in one of them, all of the other windows will be in the wrong context. This could be a problem if the user is in the middle of the multi-step form and then opens a window in the Search
context. When he switches back to the form and hits "Next", the controller will perform the action defined by the Search
context instead of the Submission
context.
There are 2 possible ways around this that I can think of:
Report
resource with the context name. So the user would visit URL's such as /search/reports
and /submission/reports/1
. This doesn't seem RESTful to me and I would rather keep the URL's as clean as possible.Are there any other ways around this problem, or better overall implementations?
I know of this project, but it's too limited for our needs.
If you want to allow for multiple contexts then obviously you must put the information which determines the current context in some storage which is not shared between tabs. Sessions, as implemented in Rack/Rails, use cookies, and cookies are shared between tabs.
Just put the context into something, that is not shared. How about a context=viewer URL parameter?
To talk REST I think it is arguable whether or not a resource is the same or not in different contexts. One could argue that a report for a "Visiting" user is different from a report for an "Administering" user. In that case a RESTy approach would probably namespace the requests (which again puts the context into the URL), e.g. /visiting/reports/1 vs /administering/reports/1.
And a third way to put the context into the URL would be to use it as part of the domain name.