Skip to content

Instantly share code, notes, and snippets.

@dhh
Last active March 15, 2024 21:41
Show Gist options
  • Save dhh/10022098 to your computer and use it in GitHub Desktop.
Save dhh/10022098 to your computer and use it in GitHub Desktop.
# config/routes.rb
resources :documents do
scope module: 'documents' do
resources :versions do
post :restore, on: :member
end
resource :lock
end
end
# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
include ProjectScoped
def index
@documents = @project.documents
end
def show
@document = @project.documents.find(params[:id])
end
def new
@document = Document.new
end
def create
@document = @project.documents.create! document_params.merge(creator: current_person)
end
end
# app/controllers/documents/locks_controller.rb
module Documents
class LocksController < ApplicationController
include DocumentScoped, ProjectScoped
def update
@document.lock!(current_person)
end
def destroy
@document.unlock!(current_person)
end
end
end
# app/controllers/documents/versions_controller.rb
module Documents
class VersionsController < ApplicationController
include DocumentScoped, ProjectScoped
before_action :set_version
def show
end
def restore
@document.restore!(@version)
end
private
def set_version
@version = @document.versions.find(params[:id])
end
end
end
# app/controllers/concerns/document_scoped.rb
module DocumentScoped
extend ActiveSupport::Concern
included do
before_action :set_document
end
private
def set_document
@document = @project.documents.find(params[:document_id])
end
end
@dhh
Copy link
Author

dhh commented Apr 7, 2014

@nambrot, yeah, same concept. It matters because ProjectScoped must be loaded first, since DocumentScoped depends on it. It's a Ruby quirk that #include loads things in reverse order, so "include DocumentScoped, ProjectScoped" will load ProjectScoped first, then DocumentScoped.

@mcmorgan, our code in Basecamp started this way, so don't have the before. Wouldn't be hard to recreate though.

@atrauzzi, when they diverge, you can't reuse the logic anyway. Per definition.

@pixeltrix
Copy link

@dhh you can simplify the routes by wrapping the nested resources with a scope, e.g:

resources :documents do
  scope module: 'documents' do
    resources :versions do
      post :restore, on: :member
    end

    resource :lock
  end
end

@kenn
Copy link

kenn commented Apr 7, 2014

@dhh Re: DocumentScoped / ProjectScoped order, is it a Ruby quirk or ActiveSupport::Concern's? I thought ActiveSupport::Concern saves dependencies in an Array and we could reverse it if that's what we want. Was there any reason that we don't do it that way?

@dhh
Copy link
Author

dhh commented Apr 16, 2014

@pixeltrix, yes of course. I've updated the gist to reflect that. Definitely nicer!

@kenn, that's a Ruby quirk. Concern is a module itself, it doesn't change how #include operates.

@seanpdoyle
Copy link

@dhh Wouldn't including ProjectScoped in DocumentScoped make the module dependency explicit, and thus allow you to remove ProjectScoped's inclusion from Documents::VersionsController and Documents::LocksController, avoiding the module ordering quirk entirely?

Also, any particular reason why you call Document.new here, instead of @project.documents.build?

@jakubstraka
Copy link

@dhh would you then apply this namespacing also for models?

So document's versions would go to document folder as version.rb with class Document::Version? Same thing with locks to lock.rb to document folder with class Document::Lock.

Final tree structure would be then

models/document/version.rb
models/document/lock

class Document::Version
   # code here
end

or wrap those classes in plural module?

models/documents/version.rb
models/documents/lock

module Documents
  class Version
    # code here
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment