Project

General

Profile

Plugin Tutorial » History » Version 15

Jean-Philippe Lang, 2008-08-10 20:29

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