Search code examples
ruby-on-railsnestedupdate-attributes

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


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

Solution

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