Plugin Tutorial » History » Version 13

Jean-Philippe Lang, 2008-08-10 19:48

1 1 Jean-Philippe Lang
h1. Plugin Tutorial
2 12 Jean-Philippe Lang
3 12 Jean-Philippe Lang
{{toc}}
4 1 Jean-Philippe Lang
5 1 Jean-Philippe Lang
h2. Creating a new Plugin
6 1 Jean-Philippe Lang
7 9 Jean-Philippe Lang
Creating a new plugin can be done using the Redmine plugin generator.
8 9 Jean-Philippe Lang
Syntax for this generator is:
9 1 Jean-Philippe Lang
10 9 Jean-Philippe Lang
  ruby script/generate redmine_plugin <plugin_name>
11 9 Jean-Philippe Lang
12 9 Jean-Philippe Lang
So open up a command prompt and "cd" to your redmine directory, then execute the following command:
13 9 Jean-Philippe Lang
14 3 Jean-Philippe Lang
  % ruby script/generate redmine_plugin Pools
15 1 Jean-Philippe Lang
16 1 Jean-Philippe Lang
The plugin structure is created in @vendor/plugins/redmine_pools@:
17 1 Jean-Philippe Lang
18 1 Jean-Philippe Lang
<pre>
19 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/app/controllers
20 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/app/helpers
21 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/app/models
22 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/app/views
23 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/db/migrate
24 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/lib/tasks
25 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/assets/images
26 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/assets/javascripts
27 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/assets/stylesheets
28 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/lang
29 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/README
30 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/init.rb
31 1 Jean-Philippe Lang
      create  vendor/plugins/redmine_pools/lang/en.yml
32 1 Jean-Philippe Lang
</pre>
33 1 Jean-Philippe Lang
34 8 Jean-Philippe Lang
Edit @vendor/plugins/redmine_pools/init.rb@ to adjust plugin information (name, author, description and version):
35 1 Jean-Philippe Lang
36 1 Jean-Philippe Lang
<pre><code class="ruby">
37 1 Jean-Philippe Lang
require 'redmine'
38 1 Jean-Philippe Lang
39 1 Jean-Philippe Lang
Redmine::Plugin.register :redmine_pools do
40 1 Jean-Philippe Lang
  name 'Pools plugin'
41 1 Jean-Philippe Lang
  author 'John Smith'
42 1 Jean-Philippe Lang
  description 'A plugin for managing pools'
43 1 Jean-Philippe Lang
  version '0.0.1'
44 1 Jean-Philippe Lang
end
45 1 Jean-Philippe Lang
</code></pre>
46 1 Jean-Philippe Lang
47 2 Jean-Philippe Lang
Then restart the application and point your browser to http://localhost:3000/admin/info.
48 1 Jean-Philippe Lang
After logging in, you should see your new plugin in the plugins list:
49 4 Jean-Philippe Lang
50 2 Jean-Philippe Lang
p=. !plugins_list1.png!
51 1 Jean-Philippe Lang
52 13 Jean-Philippe Lang
h2. Generating a model
53 13 Jean-Philippe Lang
54 13 Jean-Philippe Lang
TODO
55 13 Jean-Philippe Lang
56 1 Jean-Philippe Lang
h2. Generating a controller
57 1 Jean-Philippe Lang
58 1 Jean-Philippe Lang
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
59 9 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
60 9 Jean-Philippe Lang
61 9 Jean-Philippe Lang
  ruby script/generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]
62 9 Jean-Philippe Lang
63 9 Jean-Philippe Lang
So go back to the command prompt and run:
64 3 Jean-Philippe Lang
65 3 Jean-Philippe Lang
<pre>
66 3 Jean-Philippe Lang
% ruby script/generate redmine_plugin_controller Pools pools index vote
67 3 Jean-Philippe Lang
      exists  app/controllers/
68 3 Jean-Philippe Lang
      exists  app/helpers/
69 3 Jean-Philippe Lang
      create  app/views/pools
70 3 Jean-Philippe Lang
      create  test/functional/
71 3 Jean-Philippe Lang
      create  app/controllers/pools_controller.rb
72 3 Jean-Philippe Lang
      create  test/functional/pools_controller_test.rb
73 3 Jean-Philippe Lang
      create  app/helpers/pools_helper.rb
74 3 Jean-Philippe Lang
      create  app/views/pools/index.html.erb
75 3 Jean-Philippe Lang
      create  app/views/pools/vote.html.erb
76 3 Jean-Philippe Lang
</pre>
77 3 Jean-Philippe Lang
78 3 Jean-Philippe Lang
A controller @PoolsController@ with 2 actions (@#index@ and @#vote@) is created.
79 3 Jean-Philippe Lang
80 5 Jean-Philippe Lang
Edit @app/controllers/pools_controller.rb@ in @redmine_pools@ directory to implement these 2 actions.
81 3 Jean-Philippe Lang
82 3 Jean-Philippe Lang
<pre><code class="ruby">
83 3 Jean-Philippe Lang
class PoolsController < ApplicationController
84 3 Jean-Philippe Lang
  unloadable
85 3 Jean-Philippe Lang
  
86 3 Jean-Philippe Lang
  @@pools = [ {:id => 1, :title => 'First pool', :question => 'This is the first pool question', :yes => 0, :no => 0},
87 3 Jean-Philippe Lang
              {:id => 2, :title => 'Second pool', :question => 'This is the second pool question', :yes => 0, :no => 0} ]
88 3 Jean-Philippe Lang
89 3 Jean-Philippe Lang
  def index
90 7 Jean-Philippe Lang
    @pools = @@pools # Pool.find(:all)
91 3 Jean-Philippe Lang
  end
92 3 Jean-Philippe Lang
93 3 Jean-Philippe Lang
  def vote
94 7 Jean-Philippe Lang
    pool = @@pools.find {|p| p[:id].to_s == params[:id]} # Pool.find(params[:id])
95 3 Jean-Philippe Lang
    # saves the vote
96 3 Jean-Philippe Lang
    pool[params[:answer].to_sym] += 1
97 3 Jean-Philippe Lang
    flash[:notice] = 'Vote saved.'
98 3 Jean-Philippe Lang
    redirect_to :action => 'index'
99 3 Jean-Philippe Lang
  end
100 3 Jean-Philippe Lang
end
101 1 Jean-Philippe Lang
</code></pre>
102 5 Jean-Philippe Lang
103 5 Jean-Philippe Lang
For the sake of this example, we simulate a pool model in our @@@pools@ class variable.
104 5 Jean-Philippe Lang
We could of course use a ActiveRecord model just like we do it in a regular Rails app.
105 3 Jean-Philippe Lang
106 3 Jean-Philippe Lang
Then edit @app/views/pools/index.html.erb@ that will display existing pools:
107 3 Jean-Philippe Lang
108 3 Jean-Philippe Lang
109 3 Jean-Philippe Lang
<pre>
110 3 Jean-Philippe Lang
<h2>Pools</h2>
111 3 Jean-Philippe Lang
112 3 Jean-Philippe Lang
<% @pools.each do |pool| %>
113 3 Jean-Philippe Lang
  <p>
114 3 Jean-Philippe Lang
  <%= pool[:question] %>?
115 3 Jean-Philippe Lang
  <%= link_to 'Yes', {:action => 'vote', :id => pool[:id], :answer => 'yes'}, :method => :post %> (<%= pool[:yes] %>) /
116 3 Jean-Philippe Lang
  <%= link_to 'No', {:action => 'vote', :id => pool[:id], :answer => 'no'}, :method => :post %> (<%= pool[:no] %>)
117 3 Jean-Philippe Lang
  </p>
118 3 Jean-Philippe Lang
<% end %>
119 3 Jean-Philippe Lang
</pre>
120 3 Jean-Philippe Lang
121 3 Jean-Philippe Lang
You can remove @vote.html.erb@ since no rendering is done by the corresponding action.
122 3 Jean-Philippe Lang
123 1 Jean-Philippe Lang
Now, restart the application and point your browser to http://localhost:3000/pools.
124 4 Jean-Philippe Lang
You should see the 2 pools and you should be able to vote for them:
125 4 Jean-Philippe Lang
126 4 Jean-Philippe Lang
p=. !pools1.png!
127 4 Jean-Philippe Lang
128 4 Jean-Philippe Lang
Note that pool results are reset on each request if you don't run the application in production mode, since our pool "model" is stored in a class variable in this example.
129 4 Jean-Philippe Lang
130 4 Jean-Philippe Lang
h2. Extending menus
131 4 Jean-Philippe Lang
132 4 Jean-Philippe Lang
Our controller works fine but users have to know the url to see the pools. Using the Redmine plugin API, you can extend standard menus.
133 4 Jean-Philippe Lang
So let's add a new item to the application menu.
134 4 Jean-Philippe Lang
135 4 Jean-Philippe Lang
h3. Extending the application menu
136 4 Jean-Philippe Lang
137 4 Jean-Philippe Lang
Edit @init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
138 4 Jean-Philippe Lang
139 4 Jean-Philippe Lang
<pre><code class="ruby">
140 4 Jean-Philippe Lang
Redmine::Plugin.register :redmine_pools do
141 4 Jean-Philippe Lang
  [...]
142 4 Jean-Philippe Lang
  
143 4 Jean-Philippe Lang
  menu :application_menu, :pools, { :controller => 'pools', :action => 'index' }, :caption => 'Pools'
144 4 Jean-Philippe Lang
end
145 4 Jean-Philippe Lang
</code></pre>
146 4 Jean-Philippe Lang
147 4 Jean-Philippe Lang
Syntax is:
148 4 Jean-Philippe Lang
149 4 Jean-Philippe Lang
  menu(menu_name, item_name, url, options={})
150 4 Jean-Philippe Lang
151 4 Jean-Philippe Lang
There are 4 menus that you can extend:
152 4 Jean-Philippe Lang
153 4 Jean-Philippe Lang
* @:top_menu@ - the top left menu
154 4 Jean-Philippe Lang
* @:account_menu@ - the top right menu with sign in/sign out links
155 4 Jean-Philippe Lang
* @:application_menu@ - the main menu displayed when the user is not inside a project
156 4 Jean-Philippe Lang
* @:project_menu@ - the main menu displayed when the user is inside a project
157 4 Jean-Philippe Lang
158 4 Jean-Philippe Lang
Available options are:
159 4 Jean-Philippe Lang
160 4 Jean-Philippe Lang
* @:param@ - the parameter key that is used for the project id (default is @:id@)
161 4 Jean-Philippe Lang
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
162 4 Jean-Philippe Lang
* @:caption@ - the menu caption that can be:
163 4 Jean-Philippe Lang
164 4 Jean-Philippe Lang
  * a localized string Symbol
165 4 Jean-Philippe Lang
  * a String
166 4 Jean-Philippe Lang
  * a Proc that can take the project as argument
167 4 Jean-Philippe Lang
168 4 Jean-Philippe Lang
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
169 4 Jean-Philippe Lang
* @:last@ - if set to true, the item will stay at the end of the menu (eg. @:last => true@)
170 4 Jean-Philippe Lang
* @:html_options@ - a hash of html options that are passed to @link_to@ when rendering the menu item
171 4 Jean-Philippe Lang
172 4 Jean-Philippe Lang
In our example, we've added an item to the application menu which is emtpy by default.
173 4 Jean-Philippe Lang
Restart the application and go to http://localhost:3000:
174 4 Jean-Philippe Lang
175 4 Jean-Philippe Lang
p=. !application_menu.png!
176 4 Jean-Philippe Lang
177 4 Jean-Philippe Lang
Now you can access the pools by clicking the Pools tab from the welcome screen.
178 4 Jean-Philippe Lang
179 4 Jean-Philippe Lang
h3. Extending the project menu
180 4 Jean-Philippe Lang
181 6 Jean-Philippe Lang
Now, let's consider that the pools are defined at project level (even if it's not the case in our example pool model). So we would like to add the Pools tab to the project menu instead.
182 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
183 6 Jean-Philippe Lang
184 6 Jean-Philippe Lang
<pre><code class="ruby">
185 6 Jean-Philippe Lang
Redmine::Plugin.register :redmine_pools do
186 6 Jean-Philippe Lang
  [...]
187 6 Jean-Philippe Lang
188 6 Jean-Philippe Lang
  permission :pools, {:pools => [:index, :vote]}, :public => true
189 6 Jean-Philippe Lang
  menu :project_menu, :pools, { :controller => 'pools', :action => 'index' }, :caption => 'Pools', :after => :activity, :param => :project_id
190 6 Jean-Philippe Lang
end
191 6 Jean-Philippe Lang
</code></pre>
192 6 Jean-Philippe Lang
193 6 Jean-Philippe Lang
The second line adds our Pools tab to the project menu, just after the activity tab.
194 6 Jean-Philippe Lang
The first line is required and declares that our 2 actions from @PoolsController@ are public. We'll come back later to explain this with more details.
195 6 Jean-Philippe Lang
196 6 Jean-Philippe Lang
Restart the application again and go to one of your projects:
197 6 Jean-Philippe Lang
198 6 Jean-Philippe Lang
p=. !project_menu.png!
199 6 Jean-Philippe Lang
200 6 Jean-Philippe Lang
If you click the Pools tab, you should notice that the project menu is no longer displayed.
201 6 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
202 6 Jean-Philippe Lang
203 6 Jean-Philippe Lang
Edit your PoolsController to do so:
204 6 Jean-Philippe Lang
205 6 Jean-Philippe Lang
<pre><code class="ruby">
206 6 Jean-Philippe Lang
def index
207 6 Jean-Philippe Lang
  @project = Project.find(params[:project_id])
208 7 Jean-Philippe Lang
  @pools = @@pools # @project.pools
209 6 Jean-Philippe Lang
end
210 6 Jean-Philippe Lang
</code></pre>
211 6 Jean-Philippe Lang
212 6 Jean-Philippe Lang
The project id is available in the @:project_id@ param because of the @:param => :project_id@ option in the menu item declaration above.
213 6 Jean-Philippe Lang
214 6 Jean-Philippe Lang
Now, you should see the project menu when viewing the pools:
215 6 Jean-Philippe Lang
216 6 Jean-Philippe Lang
p=. !project_menu_pools.png!
217 4 Jean-Philippe Lang
218 4 Jean-Philippe Lang
h2. Adding new permissions
219 4 Jean-Philippe Lang
220 10 Jean-Philippe Lang
For now, anyone can vote for pools. Let's make it more configurable by changing the permission declaration.
221 10 Jean-Philippe Lang
We're going to declare 2 project based permissions, one for viewing the pools and an other one for voting. These permissions are no longer public (@:public => true@ option is removed).
222 10 Jean-Philippe Lang
223 10 Jean-Philippe Lang
Edit @init.rb@ to replace the previous permission declaration with these 2 lines:
224 10 Jean-Philippe Lang
225 10 Jean-Philippe Lang
<pre><code class="ruby">
226 10 Jean-Philippe Lang
  permission :view_pools, :pools => :index
227 10 Jean-Philippe Lang
  permission :vote_pools, :pools => :vote
228 10 Jean-Philippe Lang
</code></pre>
229 10 Jean-Philippe Lang
230 10 Jean-Philippe Lang
Restart the application and go to http://localhost:3000/roles/report:
231 10 Jean-Philippe Lang
232 10 Jean-Philippe Lang
p=. !permissions1.png!
233 10 Jean-Philippe Lang
234 10 Jean-Philippe Lang
You're now able to give these permissions to your existing roles.
235 10 Jean-Philippe Lang
236 10 Jean-Philippe Lang
Of course, some code needs to be added to the PoolsController so that actions are actually protected according to the permissions of the current user.
237 10 Jean-Philippe Lang
For this, we just need to append the @:authorize@ filter and make sure that the @project instance variable is properly set before calling this filter.
238 10 Jean-Philippe Lang
239 10 Jean-Philippe Lang
Here is how it would look like for the @#index@ action:
240 10 Jean-Philippe Lang
241 10 Jean-Philippe Lang
<pre><code class="ruby">
242 10 Jean-Philippe Lang
class PoolsController < ApplicationController
243 10 Jean-Philippe Lang
  unloadable
244 10 Jean-Philippe Lang
  
245 10 Jean-Philippe Lang
  before_filter :find_project, :authorize, :only => :index
246 10 Jean-Philippe Lang
247 10 Jean-Philippe Lang
  [...]
248 10 Jean-Philippe Lang
  
249 10 Jean-Philippe Lang
  def index
250 10 Jean-Philippe Lang
    @pools = @@pools # @project.pools
251 10 Jean-Philippe Lang
  end
252 10 Jean-Philippe Lang
253 10 Jean-Philippe Lang
  [...]
254 10 Jean-Philippe Lang
  
255 10 Jean-Philippe Lang
  private
256 10 Jean-Philippe Lang
  
257 10 Jean-Philippe Lang
  def find_project
258 10 Jean-Philippe Lang
    # @project variable must be set before calling the authorize filter
259 10 Jean-Philippe Lang
    @project = Project.find(params[:project_id])
260 10 Jean-Philippe Lang
  end
261 10 Jean-Philippe Lang
end
262 10 Jean-Philippe Lang
</code></pre>
263 10 Jean-Philippe Lang
264 10 Jean-Philippe Lang
Retrieving the current project before the @#vote@ action could be done using a similiar way.
265 10 Jean-Philippe Lang
After this, viewing and voting pools will be only available to admin users or users that have the appropriate role on the project.
266 4 Jean-Philippe Lang
267 4 Jean-Philippe Lang
h2. Creating a project module
268 4 Jean-Philippe Lang
269 11 Jean-Philippe Lang
For now, the pool functionality is added to all your projects. But you way want to enable pools for some projects only.
270 11 Jean-Philippe Lang
So, let's create a 'Pools' project module. This is done by wraping the permissions declaration inside a call to @#project_module@.
271 11 Jean-Philippe Lang
272 11 Jean-Philippe Lang
Edit @init.rb@ and change the permissions declaration:
273 11 Jean-Philippe Lang
274 11 Jean-Philippe Lang
<pre><code class="ruby">
275 11 Jean-Philippe Lang
  project_module :pools do
276 11 Jean-Philippe Lang
    permission :view_pools, :pools => :index
277 11 Jean-Philippe Lang
    permission :vote_pools, :pools => :vote
278 11 Jean-Philippe Lang
  end
279 11 Jean-Philippe Lang
</code></pre>
280 11 Jean-Philippe Lang
281 11 Jean-Philippe Lang
Restart the application and go to one of your project settings.
282 11 Jean-Philippe Lang
Click on the Modules tab. You should see the Pools module at the end of the modules list (disabled by default):
283 11 Jean-Philippe Lang
284 11 Jean-Philippe Lang
p=. !modules.png!
285 11 Jean-Philippe Lang
286 11 Jean-Philippe Lang
You can now enable/disable pools at project level.
287 11 Jean-Philippe Lang
288 11 Jean-Philippe Lang
h2. Improving the plugin views
289 11 Jean-Philippe Lang
290 11 Jean-Philippe Lang
TODO: adding stylesheet, setting page title