How to create a before_action that updates a Child's attributes Rails

1.2k views Asked by At

For this application, an App has many Elements, each of which has many Features. I'd like to update Elements attributes each time my AppsController#show runs. However, I run into a problem since I have to dig through each Element's Features. I can't seem to call @app.elements.features from my app controller (or features at all.)

I have tables embedded on the App's Show View that list an App's Elements and Features, so it's not like I can call it for a ElementsController#show or something easy like that, because then it'll never get updated. I have to find some way to call this action in the ElementsController from the AppsController, but that doesn't sound very Rails-ey to me.

This is what I've done so far, and now I'm stuck as to what I should do next. Whats the best way to get this done?

My Apps Controller (parent):

before_action :set_completion, only: :show

def set_completion
   @app = App.find(params[:id])
   @app.update_attribute(:completion_status,
   (((@app.elements.where(completion: true).count) / (@app.elements.count).to_f) * 100 )
   )
 end

Elements Controller (child):

  def update_feature_completion
  @app = App.find(params[:app_id])
      @element.update_attribute(:element_feature_completion,      
      (@element.features.where(completion: true).count)
      )
  end

EDIT: What I have so far (not simple by any means):

def set_completion
@parent = Parent.find(params[:id])

  @parent.childs.each do |child|
        if (child.childs_childs.where(completion: true).count) == (child.childs_childs.count) #if all childs_childs of the child are completed...
          then child.update_attribute(:completion, true) #...mark the child as completed.
            else child.update_attribute(:completion, false) #if all childs_childs of the child are not completed, then mark the child as not completed
        end
        #changes child completion status to % of childs_childs completed
        child.update_attribute(:child_completion_status,
          child.childs_childs.where(completion: true).count / child.childs_childs.count.to_f
          )
        @parent.update_attribute(:parent_completion_status,
            @parent.childs.child_completion_status.sum
          )

  end
end

Parent Model

class Parent < ActiveRecord::Base
  belongs_to :user
  has_many :children, dependent: :destroy
  has_many :childs_children, dependent: :destroy
  accepts_nested_attributes_for :children
  accepts_nested_attributes_for :childs_childs
end

Child Model

class Child < ActiveRecord::Base
  belongs_to :parent
  has_many :childs_children, dependent: :destroy
  accepts_nested_attributes_for :childs_children
end

Child's Child Model

class childs_child < ActiveRecord::Base
  belongs_to :child
  belongs_to :parent
end
2

There are 2 answers

8
pyRabbit On BEST ANSWER

This could end up biting you in the rear later down the road. The idea of data changing in the database every time a GET request hits the show page could cause unnecessary strain on your server. Think about all those web crawlers that hit your page. They would inadvertently trigger these database transactions to occur.

If you are going to make changes to any persisted data it should be done via the POST method. Action Callbacks are not typically meant for this type of work and should generally be used only for things like authentication and caching. So may I offer an alternative solution?

So we start with a deeply nested relationship which is fine. But instead of constantly doing these calculations when we a GET request to apps#show occurs, we have to look at what causes these changes in the first place. It appears that your data will only really change as a feature is updated as complete. That is where you should force these changes to occur. So what if you did something like this

Let's try to take a RESTful approach to this problem. As features are marked as complete, their respective Element and App should also be updated. Let's also assume that an Element can't be marked as complete without each of the features being marked as complete.

On your apps#show page I assume you are displaying all the App's Elements and the Elements subordinate features. I would imagine you would want your users to click a button on a feature that marks that feature as complete. So let's make a RESTful route that allows us to do that.

config/routes.rb

resources :features do 
  member do
    post 'complete' # features#complete
  end
end

So now we have a RESTful route that will look for the complete action inside your FeaturesController.

features_controller.rb

def complete
  if @feature.mark_as_complete
     # show some message that says "You successfully completed 'Feature A', Good job!"
  else
     #  Respond with nasty error message of why this request couldn't process
  end 
end

So now you have an action that works with that feature. Now all you have to do is create a link in your apps#show page as you iterate through each element's features

app/views/apps/show.html.haml

- @app.elements.each do |element|
  - element.features.each do |feature|
    = feature.name
    = link_to complete_feature_path(feature), method: :post

As users complete each feature they can click the link that will send a post method to features#complete action

class Feature < ActiveRecord::Base
  def mark_as_complete
    self.update_attributes(completion: true)
    self.element.update_completion_status
  end
end

class Element < ActiveRecord::Base
  def update_completion_status
    if (self.feautres.where(completion: true).count) == (self.features.count) # if all features are complete
      self.update_attributes(completion: true)
      self.update_attributes(feature_completion_status: calculate_percent_complete)
      self.app.update_completion_status
    else
      self.update_attributes(feature_completion_status: calculate_percent_complete)
    end
  end

  private
    def calculate_percent_complete
      self.features.where(completion: true).count / child.childs_childs.count.to_f
    end
end

class Feature < ActiveRecord::Base
  def mark_as_complete
    self.update_attributes(completion: true)
    self.element.update_completion_status
  end
end

class App < ActiveRecord::Base
  def update_completion_status
    # similar to that of Element I assume
  end
end

This way the appropriate data is updated only when a POST request is made to mark a feature as complete. All you should be doing in the apps#show action is provide the appropriate object to be displayed.

0
foxtrotuniform6969 On

The final bit of code looks like this:

@parent.update_attribute(:parent_completion_status,
   (@parent.children.sum(:child_completion_status) / @parent.children.count) * 100
    )

This basically finds the percentage complete that :parent_completion_status is by finding out the percentage complete that all children are.

I know it's confusing, but I believe that someone out there will be able to make sense of it. I wasn't able to solve this the way I originally wanted to when I asked the question.

It is suggested that if you are trying to do something similar, you try to follow the steps that I did instead, unless there's a simpler way that will work for you.