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:
- A user logs in.
- The user's role is
RegisteredUser
. RegisteredUser
's default context isSearch
(as defined in/app/roles/registered_user.rb
).- Inside the
Search
context, the user can only view published reports. - The user presses the "create new report" button and the context is changed to
Submission
and stored in thecurrent_user
's session. - The user then proceeds to submit a report through a multi-step form.
- Each time the user saves the report by stepping through the form the
/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:
- Namespace the
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. - Put the context name in a hidden field. This method requires developers to have remember to put the hidden field in every form on the site, and it doesn't work for GET requests.
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.