Project

General

Profile

Plugin Tutorial » History » Version 44

John Yani, 2010-12-23 21:25

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