Project

General

Profile

Plugin Tutorial » History » Version 66

Etienne Massip, 2012-05-25 11:13

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