Project

General

Profile

Plugin Tutorial » History » Version 61

Harry Garrood, 2012-01-03 12:24

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