Project

General

Profile

Plugin Tutorial » History » Version 58

Etienne Massip, 2011-12-11 00:45

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