Rails Engines and extending the issue model

Added by Dario Nuevo almost 9 years ago

Hi guys..

First: I'm a routined developer, but into Ruby since 3 days now.. So I'm a Ruby (+ Rails) n00b ;-)
Anyway, I managed to create a new plugin, use some hooks, override some views with the help of the Rails and Rails Engines documentation.. all well..

But now I'm in the situation that I want to alter the issue model. And as I've read, overriding models is such a thing which can be complicated.

The whole goal of the operation: I've got issues which estimated hours value should be the sum of all related item's estimated hours value. That's it.

But for that, I've got to modify the model to ensure that the estimated_hours value is consistend within Redmine, especially within the listings and edit view.

So I ask you guys: Do you have any hints how to realize this, how to handle model-altering in general? Best practices, anything? Even a link to some article/documentation would be great..

Regards

Replies (12)

RE: Rails Engines and extending the issue model - Added by Eric Davis almost 9 years ago

Dario Nuevo wrote:

But now I'm in the situation that I want to alter the issue model. And as I've read, overriding models is such a thing which can be complicated.

Yea, you rarely want to override a Model. Instead you should either add new methods to a Model or wrap an existing method.

The whole goal of the operation: I've got issues which estimated hours value should be the sum of all related item's estimated hours value. That's it.

But for that, I've got to modify the model to ensure that the estimated_hours value is consistend within Redmine, especially within the listings and edit view.

I do a lot of Model wrapping in my plugins. If you want to see some ideas on how to do it, checkout my Redmine plugins on my GitHub account

A quick example of adding a new method can be found on my Budget plugin. Here I add a new method to Issue called deliverable_subject and also declare a relationship.

A quick example of wrapping an existing method can be found on my Rate plugin. Here the alias_method_chain lets me hook into the UsersHelper and wrap the user_settings_tabs method. So when the Redmine core calls user_settings_tabs the codepath looks like:

  1. Redmine core calls UsersHelper#user_settings_tabs
  2. UsersHelper#user_settings_tabs runs (which is actually UsersHelper#user_settings_tabs_with_rate_tab)
  3. UsersHelper#user_settings_tabs_with_rate_tab calls the original UsersHelper#user_settings_tabs (renamed to UsersHelper#user_settings_tabs_without_rate_tab)
  4. The result then has a new Hash added to it
  5. UsersHelper#user_settings_tabs_with_rate_tab returns the combined result to the Redmine core, which is then rendered

alias_method_chain is a pretty advanced method but it's also really powerful. It took me a few months to really understand when it should be used. If you have any questions, post your code and I'll try to help you out.

Eric

RE: Rails Engines and extending the issue model - Added by Dario Nuevo over 8 years ago

Hi Eric

Thank you very much for the valuable information! ;) I'll try it out as soon I can get my hands on ruby again ;-) I'm occupied with php stuff at the moment..

Thanks for your time!
Regards
Dario

RE: Rails Engines and extending the issue model - Added by Chris Peterson over 8 years ago

Can the same method that you used to wrap an existing method in your rate plugin be used to wrap an existing method in a controller. I tried to use a similar semantic as your plugin, except use SortController instead of SortHelper. However I always end up with and error: uninitialized constant ApplicationController. I assume this has to do with the order of loading, ApplicationController is loaded with the application, which comes after the plugins. Is there anyway around this?

Thanks,

Chris

RE: Rails Engines and extending the issue model - Added by Eric Davis over 8 years ago

Try adding require_dependency 'application' to the top of your file (or require_dependency 'application_controller', Rails decided to change it's name and I forget which is right in 2.2.x).

Eric

RE: Rails Engines and extending the issue model - Added by Emilio González Montaña over 8 years ago

I've the same problem, I can't extend a controller, but with models, helpers it works...

The error running with mongrel is:

/var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:279:in `load_missing_constant': uninitialized constant ApplicationController (NameError)
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:468:in `const_missing'
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:480:in `const_missing'
        from /var/www/redmine-0.8.3/app/controllers/projects_controller.rb:18
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:216:in `load_without_new_constant_marking'
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:216:in `load_file'
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:355:in `new_constants_in'
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:215:in `load_file'
        from /var/www/redmine-0.8.3/vendor/rails/activesupport/lib/active_support/dependencies.rb:96:in `require_or_load'
         ... 33 levels...
        from /usr/lib/ruby/gems/1.8/gems/mongrel-1.1.5/bin/../lib/mongrel/command.rb:212:in `run'
        from /usr/lib/ruby/gems/1.8/gems/mongrel-1.1.5/bin/mongrel_rails:281
        from /usr/bin/mongrel_rails:19:in `load'
        from /usr/bin/mongrel_rails:19

I'm trying to extend a controller to customize Redmine from a plugin...

This is part of the code of init.rb:

require "timelog_controller_patch" 

And this is the timelog_controller_patch.rb:

require_dependency "projects_controller" 

module TimelogControllerPatch

end

ProjectsController.send(:include, TimelogControllerPatch)

Anyone has any clue about this?

RE: Rails Engines and extending the issue model - Added by Hung Nguyen about 7 years ago

Error: stack level too deep

When I use method alias_method_chain to wrap an exist method with my method. It raises this error 'stack level too deep'.

module CoreQueriesHelperPatch
  def self.included(base) # :nodoc:
    base.class_eval do
      unloadable # Send unloadable so it will not be unloaded in development

      def column_content_with_column_content_patch(column, issue)
           temp = column_content_without_column_content_patch(column, issue)
           ...<do something else>
      end

      alias_method_chain :column_content, :column_content_patch
    end
  end
end

Reason:

Follow steps above, I thinks my method runs like:
  1. Redmine core calls QueriesHelper#exist_method
  2. QueriesHelper#exist_method runs (which is actually QueriesHelper#exist_method_with_my_method)
  3. QueriesHelper#exist_method_with_my_method call the original QueriesHelper#exist_method (renamed to QueriesHelper#exist_method_without_my_method)
  4. Loop step 1...

Solution:
I try to check alias_method_chain if it run.

module CoreQueriesHelperPatch
  def self.included(base) # :nodoc:
  @is_wrap = false  
  base.class_eval do
      unloadable # Send unloadable so it will not be unloaded in development

      def column_content_with_column_content_patch(column, issue)
           temp = column_content_without_column_content_patch(column, issue)
           ...<do something else>
      end
      if !@is_wrap
         alias_method_chain :column_content, :column_content_patch
         @is_wrap = true
      end
  end
end

Hope it's useful!

RE: Rails Engines and extending the issue model - Added by Eric Davis about 7 years ago

Hung Nguyen wrote:

When I use method alias_method_chain to wrap an exist method with my method. It raises this error 'stack level too deep'.

When using alias_method_chain, I found it easier to check if the module was added in init.rb. For example:

require 'dispatcher'
Dispatcher.to_prepare :redmine_contracts do
  unless Query.included_modules.include? RedmineContracts::Patches::QueryPatch # <<<<<- Only include the module if it hasn't been already
    Query.send(:include, RedmineContracts::Patches::QueryPatch)
  end
end

http://github.com/edavis10/redmine_contracts/blob/master/init.rb#L68-70

Eric Davis

RE: Rails Engines and extending the issue model - Added by Hung Nguyen about 7 years ago

Very useful, thanks!

Regards,
Hung Nguyen

RE: Rails Engines and extending the issue model - Added by Hung Nguyen about 7 years ago

Eric Davis wrote:

Hung Nguyen wrote:

When I use method alias_method_chain to wrap an exist method with my method. It raises this error 'stack level too deep'.

When using alias_method_chain, I found it easier to check if the module was added in init.rb. For example:

[...]

http://github.com/edavis10/redmine_contracts/blob/master/init.rb#L68-70

Eric Davis

After I read code in your init.rb, I have some misunderstanding things.

1. Why don't you check all of plugin when they include patch, you just check Query, not Issue or Project. In my opinion, it will help to increase Redmine performance. When you do that, your plugin just include patch once, then they won't do again. Otherwise, each time action, helper.... was called, they also include patch again.

2. As I know, before you include patch in init.rb. You need to require '<name of patch file>'. But I don't see where you do that, or Redmine doesn't need? Can you explain it?

require 'redmine_contracts/patches/query_patch'
require_dependency 'query'
  unless Query.included_modules.include? RedmineContracts::Patches::QueryPatch
    Query.send(:include, RedmineContracts::Patches::QueryPatch)
  end

Thanks!
Hung Nguyen

RE: Rails Engines and extending the issue model - Added by Eric Davis about 7 years ago

Hung Nguyen wrote:

1. Why don't you check all of plugin when they include patch, you just check Query, not Issue or Project. In my opinion, it will help to increase Redmine performance. When you do that, your plugin just include patch once, then they won't do again. Otherwise, each time action, helper.... was called, they also include patch again.

In production the Dispatcher is only run at startup so each model is only patched once. In development mode, the performance wouldn't matter because of the class loading and unloading done by Rails. So the extra check isn't needed since it's just extra code.

2. As I know, before you include patch in init.rb. You need to require '<name of patch file>'. But I don't see where you do that, or Redmine doesn't need? Can you explain it?

Rails will autoload classes it can find in lib/. If you look at my patch names, they match the directory structure where the patches are stored. So I don't have to explicitly require them.

Eric Davis

RE: Rails Engines and extending the issue model - Added by Hung Nguyen about 7 years ago

Hi Eric,

Your information is very helpful. Thanks you so much.

Hung Nguyen

(1-12/12)