git-svn-id: http://redmine.rubyforge.org/svn/trunk@1786 e93f8b46-1217-0410-a6f0-8f06a7374b81tags/0.8.0-RC1
@@ -233,6 +233,7 @@ class IssuesController < ApplicationController | |||
issue.start_date = params[:start_date] unless params[:start_date].blank? | |||
issue.due_date = params[:due_date] unless params[:due_date].blank? | |||
issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank? | |||
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue }) | |||
# Don't save any change to the issue if the user is not authorized to apply the requested status | |||
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save | |||
# Send notification for each issue (if changed) |
@@ -89,7 +89,8 @@ module IssuesHelper | |||
when 'attachment' | |||
label = l(:label_attachment) | |||
end | |||
call_hook(:helper_issues_show_detail_after_setting, {:detail => detail, :label => label, :value => value, :old_value => old_value }) | |||
label ||= detail.prop_key | |||
value ||= detail.value | |||
old_value ||= detail.old_value |
@@ -48,4 +48,6 @@ | |||
<p><label><%=l(:label_attachment_plural)%></label><%= render :partial => 'attachments/form' %></p> | |||
<% end %> | |||
<%= call_hook(:view_issues_form_details_bottom, { :issue => @issue, :form => f }) %> | |||
<%= wikitoolbar_for 'issue_description' %> |
@@ -38,6 +38,7 @@ | |||
<label><%= l(:field_done_ratio) %>: | |||
<%= select_tag 'done_ratio', options_for_select([[l(:label_no_change_option), '']] + (0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></label> | |||
</p> | |||
<%= call_hook(:view_issues_bulk_edit_details_bottom, { :issues => @issues }) %> | |||
</fieldset> | |||
<fieldset><legend><%= l(:field_notes) %></legend> |
@@ -53,6 +53,7 @@ | |||
<%end | |||
end %> | |||
</tr> | |||
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %> | |||
</table> | |||
<hr /> | |||
@@ -6,7 +6,7 @@ | |||
<meta name="description" content="<%= Redmine::Info.app_name %>" /> | |||
<meta name="keywords" content="issue,bug,tracker" /> | |||
<%= stylesheet_link_tag 'application', :media => 'all' %> | |||
<%= javascript_include_tag :defaults %> | |||
<%= javascript_include_tag :defaults, :cache => true %> | |||
<%= stylesheet_link_tag 'jstoolbar' %> | |||
<!--[if IE]> | |||
<style type="text/css"> | |||
@@ -14,8 +14,9 @@ | |||
body {behavior: url(<%= stylesheet_path "csshover.htc" %>);} | |||
</style> | |||
<![endif]--> | |||
<!-- page specific tags --><%= yield :header_tags %> | |||
<%= call_hook :view_layouts_base_html_head %> | |||
<!-- page specific tags --> | |||
<%= yield :header_tags -%> | |||
</head> | |||
<body> | |||
<div id="wrapper"> |
@@ -10,6 +10,7 @@ | |||
<th><%= l(:label_user) %></th> | |||
<th><%= l(:label_role) %></th> | |||
<th style="width:15%"></th> | |||
<%= call_hook(:view_projects_settings_members_table_header) %> | |||
</thead> | |||
<tbody> | |||
<% members.each do |member| %> | |||
@@ -30,6 +31,7 @@ | |||
}, :title => l(:button_delete), | |||
:class => 'icon icon-del' %> | |||
</td> | |||
<%= call_hook(:view_projects_settings_members_table_row, :member => @member) %> | |||
</tr> | |||
</tbody> | |||
<% end; reset_cycle %> |
@@ -45,4 +45,6 @@ | |||
<% end %> | |||
</div> | |||
<%= call_hook :view_versions_show_bottom, :version => @version %> | |||
<% html_title @version.name %> |
@@ -0,0 +1,18 @@ | |||
Description: | |||
The plugin generator creates stubs for a new Redmine plugin. | |||
Example: | |||
./script/generate redmine_plugin meetings | |||
create vendor/plugins/redmine_meetings/app/controllers | |||
create vendor/plugins/redmine_meetings/app/helpers | |||
create vendor/plugins/redmine_meetings/app/models | |||
create vendor/plugins/redmine_meetings/app/views | |||
create vendor/plugins/redmine_meetings/db/migrate | |||
create vendor/plugins/redmine_meetings/lib/tasks | |||
create vendor/plugins/redmine_meetings/assets/images | |||
create vendor/plugins/redmine_meetings/assets/javascripts | |||
create vendor/plugins/redmine_meetings/assets/stylesheets | |||
create vendor/plugins/redmine_meetings/lang | |||
create vendor/plugins/redmine_meetings/README | |||
create vendor/plugins/redmine_meetings/init.rb | |||
create vendor/plugins/redmine_meetings/lang/en.yml |
@@ -0,0 +1,31 @@ | |||
class RedminePluginGenerator < Rails::Generator::NamedBase | |||
attr_reader :plugin_path, :plugin_name, :plugin_pretty_name | |||
def initialize(runtime_args, runtime_options = {}) | |||
super | |||
@plugin_name = "redmine_#{file_name.underscore}" | |||
@plugin_pretty_name = plugin_name.titleize | |||
@plugin_path = "vendor/plugins/#{plugin_name}" | |||
end | |||
def manifest | |||
record do |m| | |||
m.directory "#{plugin_path}/app/controllers" | |||
m.directory "#{plugin_path}/app/helpers" | |||
m.directory "#{plugin_path}/app/models" | |||
m.directory "#{plugin_path}/app/views" | |||
m.directory "#{plugin_path}/db/migrate" | |||
m.directory "#{plugin_path}/lib/tasks" | |||
m.directory "#{plugin_path}/assets/images" | |||
m.directory "#{plugin_path}/assets/javascripts" | |||
m.directory "#{plugin_path}/assets/stylesheets" | |||
m.directory "#{plugin_path}/lang" | |||
m.directory "#{plugin_path}/test" | |||
m.template 'README', "#{plugin_path}/README" | |||
m.template 'init.rb', "#{plugin_path}/init.rb" | |||
m.template 'en.yml', "#{plugin_path}/lang/en.yml" | |||
m.template 'test_helper.rb', "#{plugin_path}/test/test_helper.rb" | |||
end | |||
end | |||
end |
@@ -0,0 +1,3 @@ | |||
= <%= file_name %> | |||
Description goes here |
@@ -0,0 +1,2 @@ | |||
# English strings go here | |||
my_label: "My label" |
@@ -0,0 +1,8 @@ | |||
require 'redmine' | |||
Redmine::Plugin.register :<%= plugin_name %> do | |||
name '<%= plugin_pretty_name %> plugin' | |||
author 'Author name' | |||
description 'This is a plugin for Redmine' | |||
version '0.0.1' | |||
end |
@@ -0,0 +1,5 @@ | |||
# Load the normal Rails helper | |||
require File.expand_path(File.dirname(__FILE__) + '/../../../../test/test_helper') | |||
# Ensure that we are using the temporary fixture path | |||
Engines::Testing.set_fixture_path |
@@ -0,0 +1,5 @@ | |||
Description: | |||
Generates a plugin controller. | |||
Example: | |||
./script/generate redmine_plugin_controller MyPlugin Pools index show vote |
@@ -0,0 +1,18 @@ | |||
require 'rails_generator/base' | |||
require 'rails_generator/generators/components/controller/controller_generator' | |||
class RedminePluginControllerGenerator < ControllerGenerator | |||
attr_reader :plugin_path, :plugin_name, :plugin_pretty_name | |||
def initialize(runtime_args, runtime_options = {}) | |||
runtime_args = runtime_args.dup | |||
@plugin_name = "redmine_" + runtime_args.shift.underscore | |||
@plugin_pretty_name = plugin_name.titleize | |||
@plugin_path = "vendor/plugins/#{plugin_name}" | |||
super(runtime_args, runtime_options) | |||
end | |||
def destination_root | |||
File.join(RAILS_ROOT, plugin_path) | |||
end | |||
end |
@@ -0,0 +1,7 @@ | |||
class <%= class_name %>Controller < ApplicationController | |||
<% actions.each do |action| -%> | |||
def <%= action %> | |||
end | |||
<% end -%> | |||
end |
@@ -0,0 +1,8 @@ | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
class <%= class_name %>ControllerTest < ActionController::TestCase | |||
# Replace this with your real tests. | |||
def test_truth | |||
assert true | |||
end | |||
end |
@@ -0,0 +1,2 @@ | |||
module <%= class_name %>Helper | |||
end |
@@ -0,0 +1 @@ | |||
<h2><%= class_name %>#<%= action %></h2> |
@@ -0,0 +1,5 @@ | |||
Description: | |||
Generates a plugin model. | |||
Examples: | |||
./script/generate redmine_plugin_model MyPlugin pool title:string question:text |
@@ -0,0 +1,18 @@ | |||
require 'rails_generator/base' | |||
require 'rails_generator/generators/components/model/model_generator' | |||
class RedminePluginModelGenerator < ModelGenerator | |||
attr_accessor :plugin_path, :plugin_name, :plugin_pretty_name | |||
def initialize(runtime_args, runtime_options = {}) | |||
runtime_args = runtime_args.dup | |||
@plugin_name = "redmine_" + runtime_args.shift.underscore | |||
@plugin_pretty_name = plugin_name.titleize | |||
@plugin_path = "vendor/plugins/#{plugin_name}" | |||
super(runtime_args, runtime_options) | |||
end | |||
def destination_root | |||
File.join(RAILS_ROOT, plugin_path) | |||
end | |||
end |
@@ -0,0 +1,11 @@ | |||
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html | |||
one: | |||
id: 1 | |||
<% for attribute in attributes -%> | |||
<%= attribute.name %>: <%= attribute.default %> | |||
<% end -%> | |||
two: | |||
id: 2 | |||
<% for attribute in attributes -%> | |||
<%= attribute.name %>: <%= attribute.default %> | |||
<% end -%> |
@@ -0,0 +1,13 @@ | |||
class <%= migration_name %> < ActiveRecord::Migration | |||
def self.up | |||
create_table :<%= table_name %> do |t| | |||
<% for attribute in attributes -%> | |||
t.column :<%= attribute.name %>, :<%= attribute.type %> | |||
<% end -%> | |||
end | |||
end | |||
def self.down | |||
drop_table :<%= table_name %> | |||
end | |||
end |
@@ -0,0 +1,2 @@ | |||
class <%= class_name %> < ActiveRecord::Base | |||
end |
@@ -0,0 +1,10 @@ | |||
require File.dirname(__FILE__) + '/../test_helper' | |||
class <%= class_name %>Test < Test::Unit::TestCase | |||
fixtures :<%= table_name %> | |||
# Replace this with your real tests. | |||
def test_truth | |||
assert true | |||
end | |||
end |
@@ -4,6 +4,7 @@ require 'redmine/activity' | |||
require 'redmine/mime_type' | |||
require 'redmine/core_ext' | |||
require 'redmine/themes' | |||
require 'redmine/hook' | |||
require 'redmine/plugin' | |||
begin |
@@ -0,0 +1,109 @@ | |||
# Redmine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
module Redmine | |||
module Hook | |||
@@listener_classes = [] | |||
@@listeners = nil | |||
@@hook_listeners = {} | |||
class << self | |||
# Adds a listener class. | |||
# Automatically called when a class inherits from Redmine::Hook::Listener. | |||
def add_listener(klass) | |||
raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton) | |||
@@listener_classes << klass | |||
clear_listeners_instances | |||
end | |||
# Returns all the listerners instances. | |||
def listeners | |||
@@listeners ||= @@listener_classes.collect {|listener| listener.instance} | |||
end | |||
# Returns the listeners instances for the given hook. | |||
def hook_listeners(hook) | |||
@@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)} | |||
end | |||
# Clears all the listeners. | |||
def clear_listeners | |||
@@listener_classes = [] | |||
clear_listeners_instances | |||
end | |||
# Clears all the listeners instances. | |||
def clear_listeners_instances | |||
@@listeners = nil | |||
@@hook_listeners = {} | |||
end | |||
# Calls a hook. | |||
# Returns the listeners response. | |||
def call_hook(hook, context={}) | |||
response = '' | |||
hook_listeners(hook).each do |listener| | |||
response << listener.send(hook, context).to_s | |||
end | |||
response | |||
end | |||
end | |||
# Base class for hook listeners. | |||
class Listener | |||
include Singleton | |||
# Registers the listener | |||
def self.inherited(child) | |||
Redmine::Hook.add_listener(child) | |||
super | |||
end | |||
end | |||
# Listener class used for views hooks. | |||
# Listeners that inherit this class will include various helpers by default. | |||
class ViewListener < Listener | |||
include ERB::Util | |||
include ActionView::Helpers::TagHelper | |||
include ActionView::Helpers::FormHelper | |||
include ActionView::Helpers::FormTagHelper | |||
include ActionView::Helpers::FormOptionsHelper | |||
include ActionView::Helpers::JavaScriptHelper | |||
include ActionView::Helpers::PrototypeHelper | |||
include ActionView::Helpers::NumberHelper | |||
include ActionView::Helpers::UrlHelper | |||
include ActionView::Helpers::AssetTagHelper | |||
include ActionView::Helpers::TextHelper | |||
include ActionController::UrlWriter | |||
include ApplicationHelper | |||
end | |||
# Helper module included in ApplicationHelper so that hooks can be called | |||
# in views like this: | |||
# <%= call_hook(:some_hook) %> | |||
# <%= call_hook(:another_hook, :foo => 'bar' %> | |||
# | |||
# Current project is automatically added to the call context. | |||
module Helper | |||
def call_hook(hook, context={}) | |||
Redmine::Hook.call_hook(hook, {:project => @project}.merge(context)) | |||
end | |||
end | |||
end | |||
end | |||
ApplicationHelper.send(:include, Redmine::Hook::Helper) |
@@ -0,0 +1,38 @@ | |||
require 'source_annotation_extractor' | |||
# Modified version of the SourceAnnotationExtractor in railties | |||
# Will search for runable code that uses <tt>call_hook</tt> | |||
class PluginSourceAnnotationExtractor < SourceAnnotationExtractor | |||
# Returns a hash that maps filenames under +dir+ (recursively) to arrays | |||
# with their annotations. Only files with annotations are included, and only | |||
# those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+ | |||
# are taken into account. | |||
def find_in(dir) | |||
results = {} | |||
Dir.glob("#{dir}/*") do |item| | |||
next if File.basename(item)[0] == ?. | |||
if File.directory?(item) | |||
results.update(find_in(item)) | |||
elsif item =~ /(hook|test)\.rb/ | |||
# skip | |||
elsif item =~ /\.(builder|(r(?:b|xml|js)))$/ | |||
results.update(extract_annotations_from(item, /\s*(#{tag})\(?\s*(.*)$/)) | |||
elsif item =~ /\.(rhtml|erb)$/ | |||
results.update(extract_annotations_from(item, /<%=\s*\s*(#{tag})\(?\s*(.*?)\s*%>/)) | |||
end | |||
end | |||
results | |||
end | |||
end | |||
namespace :redmine do | |||
namespace :plugins do | |||
desc "Enumerate all Redmine plugin hooks and their context parameters" | |||
task :hook_list do | |||
PluginSourceAnnotationExtractor.enumerate 'call_hook' | |||
end | |||
end | |||
end |
@@ -316,4 +316,23 @@ class ProjectsControllerTest < Test::Unit::TestCase | |||
end | |||
end | |||
end | |||
# A hook that is manually registered later | |||
class ProjectBasedTemplate < Redmine::Hook::ViewListener | |||
def view_layouts_base_html_head(context) | |||
# Adds a project stylesheet | |||
stylesheet_link_tag(context[:project].identifier) if context[:project] | |||
end | |||
end | |||
# Don't use this hook now | |||
Redmine::Hook.clear_listeners | |||
def test_hook_response | |||
Redmine::Hook.add_listener(ProjectBasedTemplate) | |||
get :show, :id => 1 | |||
assert_tag :tag => 'link', :attributes => {:href => '/stylesheets/ecookbook.css'}, | |||
:parent => {:tag => 'head'} | |||
Redmine::Hook.clear_listeners | |||
end | |||
end |
@@ -0,0 +1,83 @@ | |||
# redMine - project management software | |||
# Copyright (C) 2006-2008 Jean-Philippe Lang | |||
# | |||
# This program is free software; you can redistribute it and/or | |||
# modify it under the terms of the GNU General Public License | |||
# as published by the Free Software Foundation; either version 2 | |||
# of the License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU General Public License | |||
# along with this program; if not, write to the Free Software | |||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
require File.dirname(__FILE__) + '/../../../test_helper' | |||
class Redmine::Hook::ManagerTest < Test::Unit::TestCase | |||
# Some hooks that are manually registered in these tests | |||
class TestHook < Redmine::Hook::Listener; end | |||
class TestHook1 < TestHook | |||
def view_layouts_base_html_head(context) | |||
'Test hook 1 listener.' | |||
end | |||
end | |||
class TestHook2 < TestHook | |||
def view_layouts_base_html_head(context) | |||
'Test hook 2 listener.' | |||
end | |||
end | |||
class TestHook3 < TestHook | |||
def view_layouts_base_html_head(context) | |||
"Context keys: #{context.keys.collect(&:to_s).sort.join(', ')}." | |||
end | |||
end | |||
Redmine::Hook.clear_listeners | |||
def setup | |||
@hook_module = Redmine::Hook | |||
end | |||
def teardown | |||
@hook_module.clear_listeners | |||
end | |||
def test_clear_listeners | |||
assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size | |||
@hook_module.add_listener(TestHook1) | |||
@hook_module.add_listener(TestHook2) | |||
assert_equal 2, @hook_module.hook_listeners(:view_layouts_base_html_head).size | |||
@hook_module.clear_listeners | |||
assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size | |||
end | |||
def test_add_listener | |||
assert_equal 0, @hook_module.hook_listeners(:view_layouts_base_html_head).size | |||
@hook_module.add_listener(TestHook1) | |||
assert_equal 1, @hook_module.hook_listeners(:view_layouts_base_html_head).size | |||
end | |||
def test_call_hook | |||
@hook_module.add_listener(TestHook1) | |||
assert_equal 'Test hook 1 listener.', @hook_module.call_hook(:view_layouts_base_html_head) | |||
end | |||
def test_call_hook_with_context | |||
@hook_module.add_listener(TestHook3) | |||
assert_equal 'Context keys: bar, foo.', @hook_module.call_hook(:view_layouts_base_html_head, :foo => 1, :bar => 'a') | |||
end | |||
def test_call_hook_with_multiple_listeners | |||
@hook_module.add_listener(TestHook1) | |||
@hook_module.add_listener(TestHook2) | |||
assert_equal 'Test hook 1 listener.Test hook 2 listener.', @hook_module.call_hook(:view_layouts_base_html_head) | |||
end | |||
end |