ruby-on-railsrubyrestdci

RESTful DCI contexts in Rails


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:

  1. A user logs in.
  2. The user's role is RegisteredUser.
  3. RegisteredUser's default context is Search (as defined in /app/roles/registered_user.rb).
  4. Inside the Search context, the user can only view published reports.
  5. The user presses the "create new report" button and the context is changed to Submission and stored in the current_user's session.
  6. The user then proceeds to submit a report through a multi-step form.
  7. 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:

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


Solution

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