ruby-on-railsruby-on-rails-3relationbefore-filter

before_destroy check if attribute for related model is given (version control)


i have a model which has_many relation to a version model. Any action on the parent model need to be tracked. At the form for delete i have added a nested form to enter a ticket number which will be added to the versions. How could i check in the model validations if the ticket is given? I will write the version before the destroy on the model is called.

# model.rb
class Model < ActiveRecord::Base
      has_many    :versions,
                  :as => :version_object
end

# models_controller.rb
def destroy                                             
    @model = Model.find(params[:id])                 
    self.write_versions('Destroy')                        
    @Model.destroy                                     

    respond_to do |format|                                
      ...
    end                                                   
  end

# delete.html.erb
<%= form_for [@model.parent_object, @model], :html => { :class => 'form-horizontal' }, :method => :delete
    do |f| %>
      <div class="form-actions">
        <%= f.fields_for :versions, @model.versions.build do |v| %>
          <%= v.text_field :ticket, {:class => 'text_field', :placeholder => 'Ticket Nummer'}%>
        <% end %>
        <%= f.submit 'Delete Model', :class => 'btn btn-danger' %>
        <%= link_to t('.cancel', :default => t("helpers.links.cancel")),
                    :back, :class => 'btn' %>
      </div>
    <% end %>

I already tried to implement a before_destroy method to check if a version with the 'Destroy' action is written but this won't work because the key fields to identify the certain action could be exist more than one time. The versions are incremental and can be rolled back step by step to get an older version and my model could have more than one relation identifier at the lifetime of his parent.

An solution would be to check the existence of the ticket at the controller through the params, but validations should be at the model.

I don't want to use a versioning gem.

Anybody a hint how to implement such a validation?


Solution

  • You could try encapsulating the versioning logic entirely in your Model class and not accessing the .versions relation in the controllers and templates at all. Consider something like this:

    # model.rb
    class Model
      has_many :versions
    
      attr_accessor :_current_action, :_ticket_number
    
      validates :_current_action, presence: true
      validates :_ticket_number, presence: true
    
      after_create :create_new_version
      before_destroy :create_new_version
    
      def set_next_action(action, ticket_number)
        self._current_action = action
        self._ticket_number = ticket_number
      end
    
      private
    
      def create_new_version
        unless _current_action.present? && _ticket_number.present?
          raise RuntimeError, "Missing versioning action or ticket number"
        end
    
        # adjust this to store the actual model data as well
        versions.create!(action: _current_action, ticket_number: _ticket_number)
    
        self._current_action = nil
        self._ticket_number = nil
      end
    end
    

    Then, this is how you do an update:

    <!-- edit.html.erb -->
    <%= form_for @model, method: :patch do |f| %>
      <!-- other fields -->
      <%= text_field_tag :_ticket_number, class: "text_field" %>
      <%= f.submit "Update model" %>
    <% end %>
    

    In the controller:

    # models_controller.rb
    def update
      @model = Model.find(params[:id])
      @model.set_next_action "Update", params[:_ticket_number]
    
      if @model.update(params[:model])
        # ...
      else
        render :edit
      end
    end
    

    This is how you do a delete:

    <!-- edit.html.erb -->
    <%= form_for @model, method: :delete do |f| %>
      <!-- other fields -->
      <%= text_field_tag :_ticket_number, class: "text_field" %>
      <%= f.submit "Delete model" %>
    <% end %>
    

    In the controller:

    # models_controller.rb
    def destroy
      @model = Model.find(params[:id])
      @model.set_next_action "Destroy", params[:_ticket_number]
    
      if @model.valid?  # validates presence of ticket number
        @model.destroy
        # ...
      else
        render :edit
      end
    end
    

    Note: I didn't test this code, so may need a few changes before it will work.