Rails - Displaying associated attributes - unpermi

2019-07-13 17:17发布

问题:

I am trying to make an app in Rails 4.

I have a profile model, a qualification model, and a vision model.

The associations are:

profile.rb

has_many :qualifications  
 accepts_nested_attributes_for :qualification 

  has_one :vision
    accepts_nested_attributes_for :vision    

qualification.rb

belongs_to :profile

vision.rb

belongs_to :profile

I have several other models in a similar construct as qualification and vision (ie. they belong to profile).

All relevant attributes have been included in the strong params for the model and where they are nested, also in the profile strong params.

def profile_params
      params[:profile].permit(:user_id, :title, :hero, :overview, :research_interest, :occupation, :external_profile, 
        :working_languages, :tag_list,
          personality_attributes: [:average_day, :fantasy_project, :preferred_style],
          vision_attributes: [:long_term, :immediate_challenge], 
          qualifications_attributes: [:id, :level, :title, :year_earned, :institution, :_destroy] )
    end

The forms are set up as:

<%= simple_form_for(@profile) do |f| %>
            <%= f.error_notification %>

              <div class="form-inputs">

<div class="intpol2">
              Your professional qualifications
            </div>

              <%= render 'qualifications/form', f: f %>     

          </div>

 <div class="intpol2">
              Your research vision
            </div>

            <%= render 'visions/form', f: f %>
          </div>

<div class="form-actions">
                <%= f.button :submit, "Submit", :class => 'formsubmit' %>
              </div>
        <% end %>

Then in the qualifications form, I have:

<%= simple_fields_for :qualification do |f| %>

  <div class="form-inputs">


    <div class="row">
        <div class="col-md-6">
            <%= f.input :title, :label => "Your award" %> 
        </div>

        <div class="col-md-6">


        </div>


    </div>

    <div class="row">
        <div class="col-md-6">
            <%= f.input :level,   collection: [ "Bachelor's degree", "Master's degree", "Ph.D", "Post Doctoral award"] %>
        </div>


        <div class="col-md-6">
        <%= f.input :year_earned, :label => "When did you graduate?", collection: (Date.today.year - 50)..(Date.today.year) %>
        </div>

  </div>




  </div>
<% end %>

And, similarly, in the vision form, I have:

<%= simple_fields_for :vision do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <%= f.input :long_term, as: :text, :label => "What is your long term research vision?", :input_html => {:rows => 10} %>

    <%= f.input :immediate_challenge, as: :text, :label => "What do you see as the immediate challenge to be addressed in pursuit of your long-term vision?",  :input_html => {:rows => 10} %>

  </div>

<% end %>

The problem is, these attributes are not saving when I press submit on the profile form. The console shows that there are no inputs in either of these tables.

Is there another step to this process?

I think the problem runs deeper. This is because I have another model called address.rb. It's polymorphic.

The associations are:

address.rb

 belongs_to :addressable, :polymorphic => true

profile.rb

has_many :addresses, as: :addressable

In my profiles show, I have:

<%= render 'profiles/geography'  %>

In my geography partial, I have:

<% if @profile.addresses.any? %>
        <%= @profile.addresses.first.country_name.titlecase %> 
    <% else %>
        <span class="profileeditlink">
            <%= link_to "Add your location", new_address_path %>
        </span>
    <% end %>   

The console shows I have 4 saved addresses. I only have one user.

The profile show page displays the else condition (i.e. add an address), even when there is an address saved.

The only attributes that successfully display on my profile show page, are those that are in the profiles table. All associated attributes just appear blank. Those in qualifications/vision, aren't saving to the db.

I can't figure out why this is all messed up.

Another angle to this problem is that my qualifications form has now saved an entry. It shows in the rails console. However, my qualifications partial which is supposed to display that entry, only shows the title.

<div class="profilequalificationstitle">
                    Qualifications
                </div>  
            </div>
        </div>


        <% Qualification.all.sort_by(&:year_earned).each do |qual| %>

        <div class="row">

                  <div class="col-md-12">
                    <div class="profilequalifications">
                        <%= @profile.try(:qualification).try(:title) %>
                    </div>  
                  </div>    
        </div>

        <div class="row">
                <div class="col-md-4">
                    <div class="intpolmin">
                        <%= @profile.try(:qualification).try(:year_earned) %>: 
                    </div>  
                </div>
                <div class="col-md-8">
                    <div class="intpolmin">
                        <%= @profile.try(:qualification).try(:institution) %>
                    </div>  
                </div>
        </div>  

          <% end %>

    </div>
</div>               

TAKING RICH PECK'S SUGGESTION BELOW:

I change the format of my strong params definition in the profiles controller so that it doesnt use the format provided by rails scaffolding and instead appears as:

def profile_params
     params.require(:profile).permit(:user_id, :title, :hero, :overview, :research_interest, :occupation, :external_profile, 
        :working_languages, :tag_list,
          user_attributes: [:avatar],
          personality_attributes: [:average_day, :fantasy_project, :preferred_style],
          vision_attributes: [:long_term, :immediate_challenge], 
          qualifications_attributes: [:id, :level, :title, :year_earned, :institution, :_destroy] )
    end

All of the associated models still use the rails scaffolding format - i.e:

def vision_params
      params[:vision].permit(:profile_id, :long_term, :immediate_challenge)
    end

The new method in the profiles controller has:

def new
    @profile = Profile.new
    @profile.qualifications.build
    @profile.vision.build
    @profile.personality.build

    authorize @profile
  end

As for the manner in which the associations are expressed, its a personal preference to keep the nested attributes with the association. I can see that this suggestion uses less lines to achieve the same thing, but my preference is to have the association with the nested attribute declaration.

My profile model associations are:

belongs_to :user
  has_many :addresses, as: :addressable

  has_one :personality
    accepts_nested_attributes_for :personality

  has_many :qualifications  
    accepts_nested_attributes_for :qualifications,  reject_if: :all_blank, allow_destroy: true

  has_one :vision
    accepts_nested_attributes_for :vision    

My profile show page then tries to use attributes from other models in a number of ways. It works successfully when I try to use attributes from the user model (profile belongs to user).

It does not work when I try to use attributes from models that belong to profile.

I have the following ways of using those attributes - none of which are working:

  1. render a partial: <%= render 'profiles/qualifications' %>

Qualifications 
    <div class="row">

              <div class="col-md-12">
                <div class="profilequalifications">
                     <!-- @qualification.award.profile  -->
                </div>  
              </div>    
    </div>
  1. directly refer to the attribute: <%= @profile.try(:vision).try(:long_term) %>

Then - my nested forms for vision and personality are in this format:

<%= f.simple_fields_for :profile do |f| %>
      <%= f.simple_fields_for :vision do |ff| %>


  <div class="form-inputs">
    <%= ff.input :long_term, as: :text, :label => "What is your long term research vision?", :input_html => {:rows => 10} %>

    <%= ff.input :immediate_challenge, as: :text, :label => "What do you see as the immediate challenge to be addressed in pursuit of your long-term vision?",  :input_html => {:rows => 10} %>

  </div>

<% end %>
<% end %>

My qualifications form is slightly different because it uses cocoon gem to add functionality to repeat that form for several qualifications.

FURTHER INVESTIGATION

I've found a clue.

In my terminal, I can see after the patch for creating the attribute in the form, there is an error which says:

Unpermitted parameter: profile

Looking into that, I've found each of these posts, suggesting the following solutions:

Solution 1: change the strong parameters in the profiles controller to include the id of the parent (in my case: profile_id):

Unpermitted Parameters: profile (NestedAttributes) - RAILS 4

I try changing:

vision_attributes: [:id, :long_term, :immediate_challenge], 

to

vision_attributes: [:id, :profile_id, :long_term, :immediate_challenge], 

Outcome: no difference - the same error is generated.

Solution 2: change the form

Nested attributes - Unpermitted parameters Rails 4

Taking this suggestion, I tried changing the vision form to:

<%= f.simple_fields_for :profile_attributes do |f| %>
      <%= f.simple_fields_for :vision, @profile.vision do |ff| %>


  <div class="form-inputs">
    <%= ff.input :long_term, as: :text, :label => "What is your long term research vision?", :input_html => {:rows => 10} %>

    <%= ff.input :immediate_challenge, as: :text, :label => "What do you see as the immediate challenge to be addressed in pursuit of your long-term vision?",  :input_html => {:rows => 10} %>

  </div>

<% end %>
<% end %>

Outcome: no difference - the same error results.

Solution 3: a further variation on the way strong parameters are expressed in the profiles controller:

Rails 4 Nested Attributes Unpermitted Parameters

change the way the nested strong params are expressed in the profiles controller from:

      vision_attributes: [:id, :profile_id, :long_term, :immediate_challenge], 

to

      :vision_attributes[:id, :profile_id, :long_term, :immediate_challenge], 

Outcome:

A new error:
syntax error, unexpected ',', expecting => ...ng_term, :immediate_challenge], ... ^ /Users/mem/cl/cf3/app/controllers/profiles_controller.rb:95: syntax error, unexpected ',', expecting keyword_end /Users/mem/cl/cf3/app/controllers/profiles_controller.rb:96: syntax error, unexpected ',', expecting keyword_end /Users/mem/cl/cf3/app/controllers/profiles_controller.rb:97: syntax error, unexpected ')', expecting keyword_end

Im stuck, lost and frustrated. Have just dropped more $$ than I can afford on codementor.io for the outcome to be that the mentor doesnt know how to help. Would love some ideas for what to try next.

ANOTHER SOLUTION

I found this article: http://www.sitepoint.com/complex-rails-forms-with-nested-attributes/#adding-an-address

Taking the approach shown in that tutorial, I tried creating a form helper which has:

module FormHelper
  def setup_profile(profile)
    profile.vision ||= Vision.new
    profile
  end
end

(I don't know what the word profile on the second line of the definition body means) but it is consistent with the approach taken in the tutorial for user/address.

I then change my profile form to:

<%= f.simple_fields_for (setup_profile(profile)) do |f| %>

              <%= render 'visions/form', f: f %>
            <% end %>  

And change my visions form partial to:

  <div class="form-inputs">
    <%= ff.input :long_term, as: :text, :label => "What is your long term research vision?", :input_html => {:rows => 10} %>

    <%= ff.input :immediate_challenge, as: :text, :label => "What do you see as the immediate challenge to be addressed in pursuit of your long-term vision?",  :input_html => {:rows => 10} %>

  </div>

When I save these changes and try again, I get this error:

undefined local variable or method `profile' for #<#<Class:0x007fe113fcd680>:0x007fe11f2769d0>

The error message points to this line of the profiles form:

    <%= f.simple_fields_for (setup_profile(profile)) do |f| %>

I don't know what this error message means, but I think it's because profile should either be '@profile' or ':profile' - I don't know why, it just often appears that way.

If i change this line to:

<%= f.simple_fields_for (setup_profile(@profile)) do |f| %>

I get this error:

undefined method `long_term' for #<Profile:0x007fe11c241c38>

I think this error means that the form is looking for an attribute called "long_term" in the profile table, instead of the vision table.

If i change this line to:

<%= f.simple_fields_for (setup_profile(@profile.vision)) do |f| %>

I get this error in my form helper:

undefined method `vision' for #<Vision:0x007fe117e95278>

Before I keep going with randomly trying things, can anyone see if there is a problem with the way I have tried to implement the solution shown in the site point article?

回答1:

Firstly, your params method needs fixing:

def profile_params
      params.require(:profile).permit(...

For clarification, you should read up about strong params on github.


You've written far too much; this is how it should look:

#app/models/profile.rb
class Profile < ActiveRecord::Base
   has_many :qualifications
   has_one :vision
   accepts_nested_attributes_for :qualifications, :vision #-> has to match association name
end

When you create a profile, you'll need to build the associative objects (unless they exist already):

#app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
   def new
      @profile = Profile.new
      @profile.qualifications.build
      @profile.build_vision
   end
end

This will allow you to use the following form:

#app/views/profiles/new.html.erb
<%= form_for @profile do |f| %>
   <%= f.fields_for :qualifications do |q| %>
      <%= q.text_field :title %>
      ... etc ...
   <% end %>
   <%= f.fields_for :vision do |v| %>
      <%= v.text_field :long_term %>
      ... etc ...
   <% end %>
   <%= f.submit %>
<% end %>

This should create a new profile & associated qualification / vision objects as required.


Your addressable polymorphic association shouldn't have any bearing on the above.

Although confusing at first, polymorphic associations basically give you the ability to reference associations which can be used in many different situations. The fact it's polymorphic doesn't matter.

IE Rails will just be using has_many :addresses whether it's as: :addressable or not.