Project

General

Profile

Plugin Tutorial » History » Version 37

Randy Syring, 2010-03-24 20:08
location of translation files changes depending on redmine version

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