Project

General

Profile

Plugin Tutorial » History » Version 69

Jean-Philippe Lang, 2012-05-28 10:37

1 1 Jean-Philippe Lang
h1. Plugin Tutorial
2 12 Jean-Philippe Lang
3 67 Jean-Philippe Lang
This tutorial is based on Redmine 2.x. You can view a previous version of this tutorial for Redmine 1.x "here":/projects/redmine/wiki/Plugin_Tutorial?version=66.
4 20 Jean-Philippe Lang
5 30 Vinod Singh
{{>toc}}
6 1 Jean-Philippe Lang
7
h2. Creating a new Plugin
8 40 Nick Peelman
 
9
You may need to set the RAILS_ENV variable in order to use the command below:
10 32 Jiří Křivánek
11
<pre>
12
$ export RAILS_ENV="production"
13
</pre>
14
15 59 Harry Garrood
On windows:
16
17
<pre>
18 68 Jean-Philippe Lang
$ set RAILS_ENV=production
19 59 Harry Garrood
</pre>
20
21 9 Jean-Philippe Lang
Creating a new plugin can be done using the Redmine plugin generator.
22
Syntax for this generator is:
23 1 Jean-Philippe Lang
24 67 Jean-Philippe Lang
<pre>ruby script/rails generate redmine_plugin <plugin_name></pre>
25 9 Jean-Philippe Lang
26 1 Jean-Philippe Lang
So open up a command prompt and "cd" to your redmine directory, then execute the following command:
27
28
<pre>
29 68 Jean-Philippe Lang
$ ruby script/rails generate redmine_plugin Polls
30 67 Jean-Philippe Lang
      create  plugins/polls/app
31
      create  plugins/polls/app/controllers
32
      create  plugins/polls/app/helpers
33
      create  plugins/polls/app/models
34
      create  plugins/polls/app/views
35
      create  plugins/polls/db/migrate
36
      create  plugins/polls/lib/tasks
37
      create  plugins/polls/assets/images
38
      create  plugins/polls/assets/javascripts
39
      create  plugins/polls/assets/stylesheets
40
      create  plugins/polls/config/locales
41
      create  plugins/polls/test
42
      create  plugins/polls/README.rdoc
43
      create  plugins/polls/init.rb
44
      create  plugins/polls/config/routes.rb
45
      create  plugins/polls/config/locales/en.yml
46
      create  plugins/polls/test/test_helper.rb
47 1 Jean-Philippe Lang
</pre>
48
49 67 Jean-Philippe Lang
The plugin structure is created in @plugins/polls@. Edit @plugins/polls/init.rb@ to adjust plugin information (name, author, description and version):
50 1 Jean-Philippe Lang
51
<pre><code class="ruby">
52 67 Jean-Philippe Lang
Redmine::Plugin.register :polls do
53 1 Jean-Philippe Lang
  name 'Polls plugin'
54
  author 'John Smith'
55
  description 'A plugin for managing polls'
56
  version '0.0.1'
57
end
58
</code></pre>
59
60
Then restart the application and point your browser to http://localhost:3000/admin/plugins.
61
After logging in, you should see your new plugin in the plugins list:
62
63
!plugins_list1.png!
64
65
h2. Generating a model
66
67
For now plugin doesn't store anything. Let's create a simple Poll model for our plugin. Syntax is:
68
69
<pre>
70 67 Jean-Philippe Lang
   ruby script/rails generate redmine_plugin_model <plugin_name> <model_name> [field[:type][:index] field[:type][:index] ...]
71 1 Jean-Philippe Lang
</pre>
72
73
So, go to the command prompt and run:
74
75
<pre>
76 68 Jean-Philippe Lang
$ ruby script/rails generate redmine_plugin_model polls poll question:string yes:integer no:integer
77 67 Jean-Philippe Lang
      create  plugins/polls/app/models/poll.rb
78
      create  plugins/polls/test/unit/poll_test.rb
79
      create  plugins/polls/db/migrate/001_create_polls.rb
80 13 Jean-Philippe Lang
</pre>
81 1 Jean-Philippe Lang
82 67 Jean-Philippe Lang
This creates the Poll model and the corresponding migration file @001_create_polls.rb@ in @plugins/polls/db/migrate@:
83 1 Jean-Philippe Lang
84 67 Jean-Philippe Lang
<pre><code class="ruby">
85
class CreatePolls < ActiveRecord::Migration
86
  def change
87
    create_table :polls do |t|
88
      t.string :question
89
      t.integer :yes, :default => 0
90
      t.integer :no, :default => 0
91
    end
92
  end
93
end
94
</code></pre>
95 1 Jean-Philippe Lang
96 67 Jean-Philippe Lang
You can adjust your migration file (eg. default values...) then migrate the database using the following command:
97 14 Jean-Philippe Lang
98 67 Jean-Philippe Lang
<pre>
99 68 Jean-Philippe Lang
$ rake redmine:plugins:migrate
100 1 Jean-Philippe Lang
101 67 Jean-Philippe Lang
Migrating polls (Polls plugin)...
102
==  CreatePolls: migrating ====================================================
103
-- create_table(:polls)
104
   -> 0.0410s
105
==  CreatePolls: migrated (0.0420s) ===========================================
106
</pre>
107 24 Eric Davis
108 64 Denny Schäfer
Note that each plugin has its own set of migrations.
109
110 24 Eric Davis
Lets add some Polls in the console so we have something to work with.  The console is where you can interactively work and examine the Redmine environment and is very informative to play around in.  But for now we just need create two Poll objects
111
112
<pre>
113 67 Jean-Philippe Lang
ruby script/rails console
114 15 Jean-Philippe Lang
>> Poll.create(:question => "Can you see this poll")
115 19 Jean-Philippe Lang
>> Poll.create(:question => "And can you see this other poll")
116 15 Jean-Philippe Lang
>> exit
117
</pre>
118
119 67 Jean-Philippe Lang
Edit @plugins/polls/app/models/poll.rb@ in your plugin directory to add a #vote method that will be invoked from our controller:
120 1 Jean-Philippe Lang
121 15 Jean-Philippe Lang
<pre><code class="ruby">
122
class Poll < ActiveRecord::Base
123 1 Jean-Philippe Lang
  def vote(answer)
124
    increment(answer == 'yes' ? :yes : :no)
125 60 Mischa The Evil
  end
126 57 Etienne Massip
end
127 9 Jean-Philippe Lang
</code></pre>
128
129 23 Jean-Baptiste Barth
h2. Generating a controller
130 9 Jean-Philippe Lang
131 67 Jean-Philippe Lang
*Warning*: Redmine does not provide the default wildcard route (@':controller/:action/:id'@). Plugins have to declare the routes they need in their proper @config/routes.rb@ file.
132 3 Jean-Philippe Lang
133
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
134 1 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
135 18 Jean-Philippe Lang
136 67 Jean-Philippe Lang
<pre>ruby script/rails generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]</pre>
137 1 Jean-Philippe Lang
138
So go back to the command prompt and run:
139
140
<pre>
141 68 Jean-Philippe Lang
$ ruby script/rails generate redmine_plugin_controller Polls polls index vote
142 67 Jean-Philippe Lang
      create  plugins/polls/app/controllers/polls_controller.rb
143
      create  plugins/polls/app/helpers/polls_helper.rb
144
      create  plugins/polls/test/functional/polls_controller_test.rb
145
      create  plugins/polls/app/views/polls/index.html.erb
146
      create  plugins/polls/app/views/polls/vote.html.erb
147 1 Jean-Philippe Lang
</pre>
148 3 Jean-Philippe Lang
149 1 Jean-Philippe Lang
A controller @PollsController@ with 2 actions (@#index@ and @#vote@) is created.
150 3 Jean-Philippe Lang
151 67 Jean-Philippe Lang
Edit @plugins/polls/app/controllers/polls_controller.rb@ to implement these 2 actions.
152 1 Jean-Philippe Lang
153
<pre><code class="ruby">
154 7 Jean-Philippe Lang
class PollsController < ApplicationController
155 19 Jean-Philippe Lang
  unloadable
156 3 Jean-Philippe Lang
157 7 Jean-Philippe Lang
  def index
158 19 Jean-Philippe Lang
    @polls = Poll.find(:all)
159 1 Jean-Philippe Lang
  end
160 21 Jean-Baptiste Barth
161 25 Eric Davis
  def vote
162
    poll = Poll.find(params[:id])
163
    poll.vote(params[:answer])
164
    if poll.save
165 3 Jean-Philippe Lang
      flash[:notice] = 'Vote saved.'
166
      redirect_to :action => 'index'
167 1 Jean-Philippe Lang
    end
168 5 Jean-Philippe Lang
  end
169 26 Eric Davis
end
170 3 Jean-Philippe Lang
</code></pre>
171
172 67 Jean-Philippe Lang
Then edit @plugins/polls/app/views/polls/index.html.erb@ that will display existing polls:
173 18 Jean-Philippe Lang
174 3 Jean-Philippe Lang
175 19 Jean-Philippe Lang
<pre>
176 3 Jean-Philippe Lang
<h2>Polls</h2>
177 19 Jean-Philippe Lang
178 50 Igor Zubkov
<% @polls.each do |poll| %>
179
  <p>
180 3 Jean-Philippe Lang
  <%= poll[:question] %>?
181
  <%= link_to 'Yes', { :action => 'vote', :id => poll[:id], :answer => 'yes' }, :method => :post %> (<%= poll[:yes] %>) /
182
  <%= link_to 'No', { :action => 'vote', :id => poll[:id], :answer => 'no' }, :method => :post %> (<%= poll[:no] %>)
183
  </p>
184 26 Eric Davis
<% end %>
185 3 Jean-Philippe Lang
</pre>
186 18 Jean-Philippe Lang
187 67 Jean-Philippe Lang
You can remove @plugins/polls/app/views/polls/vote.html.erb@ since no rendering is done by the corresponding action.
188 4 Jean-Philippe Lang
189 67 Jean-Philippe Lang
Edit @plugins/polls/config/routes.rb@ to add the 2 routes for the 2 actions:
190
191
<pre><code class="ruby">
192
get 'polls', :to => 'polls#index'
193
post 'post/:id/vote', :to => 'polls#vote'
194
</code></pre>
195
196 38 Randy Syring
Now, restart the application and point your browser to http://localhost:3000/polls.
197
You should see the 2 polls and you should be able to vote for them:
198
199
!pools1.png!
200 4 Jean-Philippe Lang
201 18 Jean-Philippe Lang
h2. Translations
202 4 Jean-Philippe Lang
203 67 Jean-Philippe Lang
The translation files must be stored in config/locales, eg. @plugins/polls/config/locales/@.
204 1 Jean-Philippe Lang
205
h2. Extending menus
206 4 Jean-Philippe Lang
207 26 Eric Davis
Our controller works fine but users have to know the url to see the polls. Using the Redmine plugin API, you can extend standard menus.
208 4 Jean-Philippe Lang
So let's add a new item to the application menu.
209
210 18 Jean-Philippe Lang
h3. Extending the application menu
211 4 Jean-Philippe Lang
212 67 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
213 18 Jean-Philippe Lang
214 4 Jean-Philippe Lang
<pre><code class="ruby">
215
Redmine::Plugin.register :redmine_polls do
216
  [...]
217
  
218
  menu :application_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls'
219
end
220
</code></pre>
221 42 Mischa The Evil
222 4 Jean-Philippe Lang
Syntax is:
223
224
  menu(menu_name, item_name, url, options={})
225
226
There are five menus that you can extend:
227 1 Jean-Philippe Lang
228 4 Jean-Philippe Lang
* @:top_menu@ - the top left menu
229
* @:account_menu@ - the top right menu with sign in/sign out links
230
* @:application_menu@ - the main menu displayed when the user is not inside a project
231
* @:project_menu@ - the main menu displayed when the user is inside a project
232
* @:admin_menu@ - the menu displayed on the Administration page (can only insert after Settings, before Plugins)
233
234
Available options are:
235
236 1 Jean-Philippe Lang
* @:param@ - the parameter key that is used for the project id (default is @:id@)
237 36 Jérémie Delaitre
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
238
* @:caption@ - the menu caption that can be:
239 4 Jean-Philippe Lang
240
  * a localized string Symbol
241
  * a String
242
  * a Proc that can take the project as argument
243 29 Vinod Singh
244 4 Jean-Philippe Lang
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
245 18 Jean-Philippe Lang
* @:first@, @:last@ - if set to true, the item will stay at the beginning/end of the menu (eg. @:last => true@)
246 4 Jean-Philippe Lang
* @:html@ - a hash of html options that are passed to @link_to@ when rendering the menu item
247
248
In our example, we've added an item to the application menu which is emtpy by default.
249 19 Jean-Philippe Lang
Restart the application and go to http://localhost:3000:
250 6 Jean-Philippe Lang
251
!application_menu.png!
252
253 18 Jean-Philippe Lang
Now you can access the polls by clicking the Polls tab from the welcome screen.
254 6 Jean-Philippe Lang
255
h3. Extending the project menu
256 51 Igor Zubkov
257 18 Jean-Philippe Lang
Now, let's consider that the polls are defined at project level (even if it's not the case in our example poll model). So we would like to add the Polls tab to the project menu instead.
258 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
259
260
<pre><code class="ruby">
261 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
262
  [...]
263 6 Jean-Philippe Lang
264
  permission :polls, { :polls => [:index, :vote] }, :public => true
265
  menu :project_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
266 1 Jean-Philippe Lang
end
267 6 Jean-Philippe Lang
</code></pre>
268
269 67 Jean-Philippe Lang
The second line adds our Polls tab to the project menu, just after the activity tab. The first line is required and declares that our 2 actions from @PollsController@ are public. We'll come back later to explain this with more details. Restart the application again and go to one of your projects:
270 6 Jean-Philippe Lang
271 1 Jean-Philippe Lang
!http://www.redmine.org/attachments/3773/project_menu.png!
272 6 Jean-Philippe Lang
273 67 Jean-Philippe Lang
If you click the Polls tab (in 3rd position), you should notice that the project menu is no longer displayed.
274 19 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
275 6 Jean-Philippe Lang
276 61 Harry Garrood
Edit your PollsController to do so:
277
278 6 Jean-Philippe Lang
<pre><code class="ruby">
279
def index
280 18 Jean-Philippe Lang
  @project = Project.find(params[:project_id])
281 6 Jean-Philippe Lang
  @polls = Poll.find(:all) # @project.polls
282 39 Ric Turley
end
283 4 Jean-Philippe Lang
</code></pre>
284
285
The project id is available in the @:project_id@ param because of the @:param => :project_id@ option in the menu item declaration above.
286 18 Jean-Philippe Lang
287
Now, you should see the project menu when viewing the polls:
288 10 Jean-Philippe Lang
289 26 Eric Davis
!http://www.redmine.org/attachments/3774/project_menu_pools.png!
290 10 Jean-Philippe Lang
291
h2. Adding new permissions
292 20 Jean-Philippe Lang
293 18 Jean-Philippe Lang
For now, anyone can vote for polls. Let's make it more configurable by changing the permission declaration.
294
We're going to declare 2 project based permissions, one for viewing the polls and an other one for voting. These permissions are no longer public (@:public => true@ option is removed).
295 1 Jean-Philippe Lang
296 67 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ to replace the previous permission declaration with these 2 lines:
297 10 Jean-Philippe Lang
298
<pre><code class="ruby">
299
  permission :view_polls, :polls => :index
300 29 Vinod Singh
  permission :vote_polls, :polls => :vote
301 1 Jean-Philippe Lang
</code></pre>
302 10 Jean-Philippe Lang
303
Restart the application and go to http://localhost:3000/roles/report:
304 18 Jean-Philippe Lang
305 10 Jean-Philippe Lang
!permissions1.png!
306
307
You're now able to give these permissions to your existing roles.
308 1 Jean-Philippe Lang
309 67 Jean-Philippe Lang
Of course, some code needs to be added to the PollsController so that actions are actually protected according to the permissions of the current user. 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.
310 18 Jean-Philippe Lang
311 10 Jean-Philippe Lang
Here is how it would look like for the @#index@ action:
312
313
<pre><code class="ruby">
314
class PollsController < ApplicationController
315
  unloadable
316
  
317
  before_filter :find_project, :authorize, :only => :index
318 19 Jean-Philippe Lang
319 10 Jean-Philippe Lang
  [...]
320
  
321
  def index
322
    @polls = Poll.find(:all) # @project.polls
323
  end
324
325
  [...]
326
  
327
  private
328
  
329
  def find_project
330
    # @project variable must be set before calling the authorize filter
331
    @project = Project.find(params[:project_id])
332 18 Jean-Philippe Lang
  end
333 4 Jean-Philippe Lang
end
334 31 Markus Bockman
</code></pre>
335 1 Jean-Philippe Lang
336 37 Randy Syring
Retrieving the current project before the @#vote@ action could be done using a similar way.
337 31 Markus Bockman
After this, viewing and voting polls will be only available to admin users or users that have the appropriate role on the project.
338
339
If you want to display the symbols of your permissions in a multilangual way, you need to add the necessary text labels in a language file.
340 67 Jean-Philippe Lang
Simply create an *.yml (eg. @en.yml@) file in @plugins/polls/config/locales@ and fill it with labels like this:
341 31 Markus Bockman
342
<pre><code class="ruby">
343
  permission_view_polls: View Polls
344
  permission_vote_polls: Vote Polls
345
</code></pre>
346
347 67 Jean-Philippe Lang
In this example the created file is known as @en.yml@, but all other supported language files are also possible too.
348 31 Markus Bockman
As you can see on the example above, the labels consists of the permission symbols @:view_polls@ and @:vote_polls@ with an additional @permission_@ added at the front. 
349
350 4 Jean-Philippe Lang
Restart your application and point the permission section.
351
352 56 Thomas Winkel
h2. Creating a project module
353 26 Eric Davis
354 11 Jean-Philippe Lang
For now, the poll functionality is added to all your projects. But you may want to enable polls for some projects only.
355
So, let's create a 'Polls' project module. This is done by wrapping the permissions declaration inside a call to @#project_module@.
356
357
Edit @init.rb@ and change the permissions declaration:
358 18 Jean-Philippe Lang
359
<pre><code class="ruby">
360
  project_module :polls do
361 11 Jean-Philippe Lang
    permission :view_polls, :polls => :index
362
    permission :vote_polls, :polls => :vote
363
  end
364
</code></pre>
365 18 Jean-Philippe Lang
366 11 Jean-Philippe Lang
Restart the application and go to one of your project settings.
367 29 Vinod Singh
Click on the Modules tab. You should see the Polls module at the end of the modules list (disabled by default):
368 11 Jean-Philippe Lang
369 18 Jean-Philippe Lang
!modules.png!
370 11 Jean-Philippe Lang
371
You can now enable/disable polls at project level.
372
373 16 Jean-Philippe Lang
h2. Improving the plugin views
374
375
h3. Adding stylesheets
376 26 Eric Davis
377 16 Jean-Philippe Lang
Let's start by adding a stylesheet to our plugin views.
378 67 Jean-Philippe Lang
Create a file named @voting.css@ in the @plugins/polls/assets/stylesheets@ directory:
379 16 Jean-Philippe Lang
380
<pre>
381
a.vote { font-size: 120%; }
382
a.vote.yes { color: green; }
383
a.vote.no  { color: red; }
384 18 Jean-Philippe Lang
</pre>
385 16 Jean-Philippe Lang
386 67 Jean-Philippe Lang
When starting the application, plugin assets are automatically copied to @public/plugin_assets/polls/@ to make them available through your web server. So any change to your plugin stylesheets or javascripts needs an application restart.
387 16 Jean-Philippe Lang
388 67 Jean-Philippe Lang
Then, append the following lines at the end of @plugins/polls/app/views/polls/index.html.erb@ so that your stylesheet get included in the page header by Redmine:
389 16 Jean-Philippe Lang
390 18 Jean-Philippe Lang
<pre>
391 16 Jean-Philippe Lang
<% content_for :header_tags do %>
392
    <%= stylesheet_link_tag 'voting', :plugin => 'redmine_polls' %>
393
<% end %>
394 18 Jean-Philippe Lang
</pre>
395 16 Jean-Philippe Lang
396
Note that the @:plugin => 'redmine_polls'@ option is required when calling the @stylesheet_link_tag@ helper.
397
398
Javascripts can be included in plugin views using the @javascript_include_tag@ helper in the same way.
399
400
h3. Setting page title
401
402
You can set the HTML title from inside your views by using the @html_title@ helper.
403 53 Igor Zubkov
Example:
404 34 Tom Bostelmann
405
  <% html_title "Polls" %>
406
407
h2. Testing your plugin
408
409
h3. test/test_helper.rb:
410
411
Here are the contents of my test helper file:
412
413 1 Jean-Philippe Lang
<pre>
414 69 Jean-Philippe Lang
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
415 34 Tom Bostelmann
</pre>
416
417 1 Jean-Philippe Lang
h3. Sample test:
418 34 Tom Bostelmann
419 69 Jean-Philippe Lang
Contents of polls_controller_test.rb:
420 1 Jean-Philippe Lang
421
<pre><code class="ruby">
422 69 Jean-Philippe Lang
require File.expand_path('../../test_helper', __FILE__)
423 34 Tom Bostelmann
424 69 Jean-Philippe Lang
class PollsControllerTest < ActionController::TestCase
425
  fixtures :projects
426 34 Tom Bostelmann
427 69 Jean-Philippe Lang
  def test_index
428
    get :index, :project_id => 1
429 34 Tom Bostelmann
430 69 Jean-Philippe Lang
    assert_response 'success'
431
    assert_template 'index'
432 34 Tom Bostelmann
  end
433 52 Igor Zubkov
end
434 54 Igor Zubkov
</code></pre>
435 34 Tom Bostelmann
436
h3. Initialize Test DB:
437
438
I found it easiest to initialize the test db directly with the following rake call:
439 43 David Fischer
440 34 Tom Bostelmann
<pre>
441 68 Jean-Philippe Lang
$ rake db:drop db:create db:migrate db:migrate_plugins redmine:load_default_data RAILS_ENV=test
442 34 Tom Bostelmann
</pre>
443
444 48 Igor Zubkov
h3. Run test:
445 34 Tom Bostelmann
446 69 Jean-Philippe Lang
To execute the polls_controller_test.rb:
447 34 Tom Bostelmann
448
<pre>
449 69 Jean-Philippe Lang
$ ruby plugins\polls\test\functionals\polls_controller_test.rb
450 47 Mo Morsi
</pre>
451 35 Tom Bostelmann
452
h3. Testing with permissions
453
454 1 Jean-Philippe Lang
If your plugin requires membership to a project, add the following to the beginning of your functional tests:
455 47 Mo Morsi
456
<pre>
457
def test_index
458 1 Jean-Philippe Lang
  @request.session[:user_id] = 2
459 47 Mo Morsi
  ...
460 1 Jean-Philippe Lang
end
461 47 Mo Morsi
</pre>
462
463
If your plugin requires a specific permission, you can add that to a user role like so (lookup which role is appropriate for the user in the fixtures):
464 1 Jean-Philippe Lang
465 47 Mo Morsi
<pre>
466
def test_index
467 1 Jean-Philippe Lang
  Role.find(1).add_permission! :my_permission
468
  ...
469 35 Tom Bostelmann
end
470 47 Mo Morsi
</pre>
471
472
473
You may enable/disable a specific module like so:
474
475
<pre>
476
def test_index
477
  Project.find(1).enabled_module_names = [:mymodule]
478
  ...
479 1 Jean-Philippe Lang
end
480
</pre>