简体   繁体   中英

Rails copy a record to another table

I have a table called jobplans and one called workorders . They have many similar fields (columns).

I would like to have a button that would invoke code create a new workorder and copy the similar fields from the jobplan .

I found this code like this in another question:

@jobplan = Jobplan.find(params[:id]) # find original object
@workorder = Workorder.create(@jobplan.attributes)

But -

Will that code work? If yes, where does it go? Can it go on the view page that has the button? Controller? Model?

Thanks for your help!

I'd create a controller action, say, copy_to_workorder and bind this button to that controller action. I think it makes sense for this action to live on the JobplanController .

So, at a high-level:

  1. Your view would have a form that submits a request (likely a POST ) to jobplan/:id/copy_to_workorder . You'd create this route as well.

  2. Your JobplanController would have a copy_to_workorder action which would essentially do your code above, although it would be a good idea to have some additional validations on this.

  3. The copy_to_workorder action would then do something ... Maybe redirect to the JobplanController#index action?

It may also be cleaner to create an instance method on Jobplan , maybe say create_workorder! which would do the two lines of code above.

At a glance it looks to me like the code you posted would work. Where it belongs is a more interesting question.

My first thought is to consider if these model classes are doing a good job of modelling the problem domain your application exists to serve. Multiple models with near-identical sets of fields seems like a warning sign that those common attributes should live on one model and the different stages of your workflow should be represented by a different class.

Guessing at domain terms here. Would it be a better fit to have some Plan which has a JobStatus which tracks those changes in workflow state? Could Jobplan and Workorder both have one Blueprint (or whatever captures these shared attributes)?


Regardless, let's assume these models are a good fit for the way you talk about and work with the core concepts in this domain. Where then should this behavior live and how do we invoke it?

We can start by describing the behavior we want to user to see, regardless of how we implement it. For example if the app uses Capybara feature specs we might write something like:

feature 'the job plan show page' do
  given(:user) { FactoryGirl.create :job_supervisor }
  given(:jobplan) { FactoryGirl.create :jobplan }

  background do
    login user
  end

  # ...

  scenario 'creating a work order from the job plan' do
    visit jobplan_path(jobplan)
    click_link 'Create work order'
    expect(page).to have_content 'Work order created'
    expect(current_path).to match /\/workorders\/\d/
    # ...
  end
end

This test will not pass yet but we'll get there.

I find it helpful to try to restrict Rails controllers to just the basic CRUD (Create, Read, Update, Delete) actions. If the app just exposes an API those map to the create , index or show , update , and destroy actions. When the app is serving HTML pages directly then we can add the edit and new actions as well to serve the interfaces for the create and update endpoints. Any time we expand beyond those 7 actions it is a warning that we have perhaps not done a good job modelling the problem and our controller is taking on too many responsibilities.

In this case we know we want to create a new Workorder . Creating Workorder s already has a well defined home; the create action of the WorkordersController so let's try to implement this behavior there.

To create a Workorder we need a bunch of attributes. We could have the client include them in the POST request to this create action but that leaves them under the client's control. If the client is not supposed to change these values from whatever they were on the corresponding Jobplan then that seems like a bad idea. Instead it seems preferable to receive just the Jobplan 's id , as already shown in the question.

We might just pass the Jobplan id as a POST param but I think there is a more elegant solution. By declaring Workorder as a nested resource under Jobplan we can express the relationship between these models in our URLs. This leads to some very natural responses, like a 404 response making sense if you try to create a Workorder with a Jobplan id which doesn't exist.

We can of course describe the specific behavior we want from our controller in a test as well:

describe WorkorderController do
  let(:jobplan) { FactoryGirl.create :jobplan }

  describe '#create' do
    context 'given a valid jobplan id' do
      it 'creates a new workorder' do
        expect { post jobplan_id: jobplan.id }.to change {Workorder.count}.by(1)
      end

      it 'redirects to the new workorder' do
        post jobplan_id: jobplan.id
        expect(response).to be_redirect
      end

      it 'copies the jobplan attributes to the new workorder' do
        post jobplan_id: jobplan.id
        expect(assigns(:workorder).location).to eq jobplan.location
        # ...
      end
    end
  end
end

This will fail since the route we are trying to reach is undefined so we can start there.

resources :jobplans do
  resources :workorders, only: [:create]
end
resources :workorders, only: [:show]

Will allow us to POST to /jobplans/<jobplan_id>/workorders to create a new workorder. Note that here we have declared our routes such that it is appropriate to create a workorder under a jobplan but all other allowed actions still use the toplevel workorders routes. In the future we might come back and expand these options; perhaps we will want to be able to GET /jobplans/<jobplan_id>/workorders for a list of all workorders under a specific jobplan while /workorders returns every workorder in the system.

Now we can implement the controller action.

def create
  jobplan = Jobplan.find(params[:jobplan_id])
  @workorder = Workorder.create(@jobplan.attributes)
  redirect_to workorder_path(@workorder)
end

As a final thought we probably do not want to lose track of which Jobplan was used to create a given Workorder so we might add that relationship to our models and clean up a few things. Creating workorders from jobplans is getting complicated so we are probably asking the controller to do too much. We might move the responsibility for creating a workorder into the model or a service object if it is going to have side effects like needing to send off email announcements of the change or update billing. Ultimately our controller action might look something like:

def create
  jobplan = Jobplan.find(params[:jobplan_id])
  @workorder = Workorder.create_from_jobplan(jobplan)
  redirect_to workorder_path(@workorder)
end

Hopefully that seems like a reasonable path and place to end up. I've made a number of assumptions about what your app needs and what tools you might be using as well as not actually run any of the code above and left out many of the other tests I might normally write but I trust you can decide to apply it or not as appropriate. I don't want to make this answer any longer by ranting about tests but if the content above is unfamiliar I like Jared's description of outside in testing Rails development .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM