Project

General

Profile

Plugin Tutorial » History » Version 103

luigifab !, 2017-02-12 12:05

1 1 Jean-Philippe Lang
h1. Plugin Tutorial
2 12 Jean-Philippe Lang
3 67 Jean-Philippe Lang
This tutorial is based on Redmine 2.x. You can view a previous version of this tutorial for Redmine 1.x "here":/projects/redmine/wiki/Plugin_Tutorial?version=66.
4 72 Jean-Philippe Lang
It assumes that you're familiar with the Ruby on Rails framework.
5 20 Jean-Philippe Lang
6 99 Toshi MARUYAMA
h3. NOTE: Redmine 3.x (Rails 4) script
7
8
This wiki uses @ruby script/rails@ on Redmine 2.x (Rails 3).
9
You need to use @ruby bin/rails@ or @rails@ on Redmine 3.x (Rails 4).
10
11 30 Vinod Singh
{{>toc}}
12 1 Jean-Philippe Lang
13
h2. Creating a new Plugin
14 40 Nick Peelman
 
15
You may need to set the RAILS_ENV variable in order to use the command below:
16 32 Jiří Křivánek
17
<pre>
18
$ export RAILS_ENV="production"
19
</pre>
20
21 59 Harry Garrood
On windows:
22
23
<pre>
24 68 Jean-Philippe Lang
$ set RAILS_ENV=production
25 59 Harry Garrood
</pre>
26
27 9 Jean-Philippe Lang
Creating a new plugin can be done using the Redmine plugin generator.
28
Syntax for this generator is:
29 1 Jean-Philippe Lang
30 102 Vincent Robert
<pre>bundle exec ruby bin/rails generate redmine_plugin <plugin_name></pre>
31 9 Jean-Philippe Lang
32 1 Jean-Philippe Lang
So open up a command prompt and "cd" to your redmine directory, then execute the following command:
33
34
<pre>
35 100 Toshi MARUYAMA
$ bundle exec ruby script/rails generate redmine_plugin Polls
36 67 Jean-Philippe Lang
      create  plugins/polls/app
37
      create  plugins/polls/app/controllers
38
      create  plugins/polls/app/helpers
39
      create  plugins/polls/app/models
40
      create  plugins/polls/app/views
41
      create  plugins/polls/db/migrate
42
      create  plugins/polls/lib/tasks
43
      create  plugins/polls/assets/images
44
      create  plugins/polls/assets/javascripts
45
      create  plugins/polls/assets/stylesheets
46
      create  plugins/polls/config/locales
47
      create  plugins/polls/test
48
      create  plugins/polls/README.rdoc
49
      create  plugins/polls/init.rb
50
      create  plugins/polls/config/routes.rb
51
      create  plugins/polls/config/locales/en.yml
52
      create  plugins/polls/test/test_helper.rb
53 1 Jean-Philippe Lang
</pre>
54
55 67 Jean-Philippe Lang
The plugin structure is created in @plugins/polls@. Edit @plugins/polls/init.rb@ to adjust plugin information (name, author, description and version):
56 1 Jean-Philippe Lang
57
<pre><code class="ruby">
58 67 Jean-Philippe Lang
Redmine::Plugin.register :polls do
59 1 Jean-Philippe Lang
  name 'Polls plugin'
60
  author 'John Smith'
61
  description 'A plugin for managing polls'
62
  version '0.0.1'
63
end
64
</code></pre>
65
66
Then restart the application and point your browser to http://localhost:3000/admin/plugins.
67
After logging in, you should see your new plugin in the plugins list:
68
69 71 Jean-Philippe Lang
p=. !plugins_list1.png!
70 1 Jean-Philippe Lang
71 72 Jean-Philippe Lang
Note: any change to the @init.rb@ file of your plugin requires to restart the application as it is not reloaded on each request.
72
73 1 Jean-Philippe Lang
h2. Generating a model
74
75
For now plugin doesn't store anything. Let's create a simple Poll model for our plugin. Syntax is:
76
77
<pre>
78 100 Toshi MARUYAMA
   bundle exec ruby script/rails generate redmine_plugin_model <plugin_name> <model_name> [field[:type][:index] field[:type][:index] ...]
79 1 Jean-Philippe Lang
</pre>
80
81
So, go to the command prompt and run:
82
83
<pre>
84 100 Toshi MARUYAMA
$ bundle exec ruby script/rails generate redmine_plugin_model polls poll question:string yes:integer no:integer
85 67 Jean-Philippe Lang
      create  plugins/polls/app/models/poll.rb
86
      create  plugins/polls/test/unit/poll_test.rb
87
      create  plugins/polls/db/migrate/001_create_polls.rb
88 13 Jean-Philippe Lang
</pre>
89 1 Jean-Philippe Lang
90 67 Jean-Philippe Lang
This creates the Poll model and the corresponding migration file @001_create_polls.rb@ in @plugins/polls/db/migrate@:
91 1 Jean-Philippe Lang
92 67 Jean-Philippe Lang
<pre><code class="ruby">
93
class CreatePolls < ActiveRecord::Migration
94
  def change
95
    create_table :polls do |t|
96
      t.string :question
97
      t.integer :yes, :default => 0
98
      t.integer :no, :default => 0
99
    end
100
  end
101
end
102
</code></pre>
103 1 Jean-Philippe Lang
104 67 Jean-Philippe Lang
You can adjust your migration file (eg. default values...) then migrate the database using the following command:
105 14 Jean-Philippe Lang
106 67 Jean-Philippe Lang
<pre>
107 100 Toshi MARUYAMA
$ bundle exec rake redmine:plugins:migrate
108 1 Jean-Philippe Lang
109 67 Jean-Philippe Lang
Migrating polls (Polls plugin)...
110
==  CreatePolls: migrating ====================================================
111
-- create_table(:polls)
112
   -> 0.0410s
113
==  CreatePolls: migrated (0.0420s) ===========================================
114
</pre>
115 24 Eric Davis
116 64 Anonymous
Note that each plugin has its own set of migrations.
117
118 24 Eric Davis
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
119
120
<pre>
121 100 Toshi MARUYAMA
bundle exec ruby script/rails console
122 77 mina Beshay
[rails 3] rails console
123 15 Jean-Philippe Lang
>> Poll.create(:question => "Can you see this poll")
124 19 Jean-Philippe Lang
>> Poll.create(:question => "And can you see this other poll")
125 15 Jean-Philippe Lang
>> exit
126
</pre>
127
128 67 Jean-Philippe Lang
Edit @plugins/polls/app/models/poll.rb@ in your plugin directory to add a #vote method that will be invoked from our controller:
129 1 Jean-Philippe Lang
130 15 Jean-Philippe Lang
<pre><code class="ruby">
131
class Poll < ActiveRecord::Base
132 1 Jean-Philippe Lang
  def vote(answer)
133
    increment(answer == 'yes' ? :yes : :no)
134 60 Mischa The Evil
  end
135 57 Etienne Massip
end
136 9 Jean-Philippe Lang
</code></pre>
137
138 67 Jean-Philippe Lang
h2. Generating a controller
139 3 Jean-Philippe Lang
140
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
141 1 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
142 18 Jean-Philippe Lang
143 100 Toshi MARUYAMA
<pre>bundle exec ruby script/rails generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]</pre>
144 1 Jean-Philippe Lang
145
So go back to the command prompt and run:
146
147
<pre>
148 101 Toshi MARUYAMA
$ bundle exec ruby script/rails generate redmine_plugin_controller Polls polls index vote
149 67 Jean-Philippe Lang
      create  plugins/polls/app/controllers/polls_controller.rb
150
      create  plugins/polls/app/helpers/polls_helper.rb
151
      create  plugins/polls/test/functional/polls_controller_test.rb
152
      create  plugins/polls/app/views/polls/index.html.erb
153
      create  plugins/polls/app/views/polls/vote.html.erb
154 1 Jean-Philippe Lang
</pre>
155 3 Jean-Philippe Lang
156 1 Jean-Philippe Lang
A controller @PollsController@ with 2 actions (@#index@ and @#vote@) is created.
157 3 Jean-Philippe Lang
158 67 Jean-Philippe Lang
Edit @plugins/polls/app/controllers/polls_controller.rb@ to implement these 2 actions.
159 1 Jean-Philippe Lang
160
<pre><code class="ruby">
161 7 Jean-Philippe Lang
class PollsController < ApplicationController
162 1 Jean-Philippe Lang
  unloadable
163 3 Jean-Philippe Lang
164 7 Jean-Philippe Lang
  def index
165 72 Jean-Philippe Lang
    @polls = Poll.all
166 1 Jean-Philippe Lang
  end
167 21 Jean-Baptiste Barth
168 25 Eric Davis
  def vote
169
    poll = Poll.find(params[:id])
170 1 Jean-Philippe Lang
    poll.vote(params[:answer])
171 25 Eric Davis
    if poll.save
172 3 Jean-Philippe Lang
      flash[:notice] = 'Vote saved.'
173
    end
174 72 Jean-Philippe Lang
    redirect_to :action => 'index'
175 5 Jean-Philippe Lang
  end
176 26 Eric Davis
end
177 3 Jean-Philippe Lang
</code></pre>
178
179 67 Jean-Philippe Lang
Then edit @plugins/polls/app/views/polls/index.html.erb@ that will display existing polls:
180 3 Jean-Philippe Lang
181 74 Etienne Massip
<pre><code class="erb">
182 3 Jean-Philippe Lang
<h2>Polls</h2>
183 19 Jean-Philippe Lang
184 50 Igor Zubkov
<% @polls.each do |poll| %>
185 1 Jean-Philippe Lang
  <p>
186 72 Jean-Philippe Lang
  <%= poll.question %>?
187
  <%= link_to 'Yes', { :action => 'vote', :id => poll[:id], :answer => 'yes' }, :method => :post %> (<%= poll.yes %>) /
188
  <%= link_to 'No', { :action => 'vote', :id => poll[:id], :answer => 'no' }, :method => :post %> (<%= poll.no %>)
189 1 Jean-Philippe Lang
  </p>
190 3 Jean-Philippe Lang
<% end %>
191 74 Etienne Massip
</code></pre>
192 26 Eric Davis
193 72 Jean-Philippe Lang
You can remove @plugins/polls/app/views/polls/vote.html.erb@ since no rendering is done by the @#vote@ action.
194 18 Jean-Philippe Lang
195 72 Jean-Philippe Lang
h3. Adding routes
196 1 Jean-Philippe Lang
197 72 Jean-Philippe Lang
Redmine does not provide the default wildcard route (@':controller/:action/:id'@). Plugins have to declare the routes they need in their proper @config/routes.rb@ file. So edit @plugins/polls/config/routes.rb@ to add the 2 routes for the 2 actions:
198
199 67 Jean-Philippe Lang
<pre><code class="ruby">
200
get 'polls', :to => 'polls#index'
201 1 Jean-Philippe Lang
post 'post/:id/vote', :to => 'polls#vote'
202 67 Jean-Philippe Lang
</code></pre>
203
204 72 Jean-Philippe Lang
You can find more information about Rails routes here: http://guides.rubyonrails.org/routing.html.
205
206 38 Randy Syring
Now, restart the application and point your browser to http://localhost:3000/polls.
207 1 Jean-Philippe Lang
You should see the 2 polls and you should be able to vote for them:
208 38 Randy Syring
209 71 Jean-Philippe Lang
p=. !pools1.png!
210 4 Jean-Philippe Lang
211 72 Jean-Philippe Lang
h2. Internationalization
212 4 Jean-Philippe Lang
213 67 Jean-Philippe Lang
The translation files must be stored in config/locales, eg. @plugins/polls/config/locales/@.
214 1 Jean-Philippe Lang
215
h2. Extending menus
216 4 Jean-Philippe Lang
217 26 Eric Davis
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.
218 4 Jean-Philippe Lang
So let's add a new item to the application menu.
219
220 18 Jean-Philippe Lang
h3. Extending the application menu
221 4 Jean-Philippe Lang
222 67 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
223 18 Jean-Philippe Lang
224 4 Jean-Philippe Lang
<pre><code class="ruby">
225
Redmine::Plugin.register :redmine_polls do
226
  [...]
227
  
228
  menu :application_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls'
229
end
230
</code></pre>
231 42 Mischa The Evil
232 4 Jean-Philippe Lang
Syntax is:
233
234
  menu(menu_name, item_name, url, options={})
235
236
There are five menus that you can extend:
237 1 Jean-Philippe Lang
238 4 Jean-Philippe Lang
* @:top_menu@ - the top left menu
239
* @:account_menu@ - the top right menu with sign in/sign out links
240
* @:application_menu@ - the main menu displayed when the user is not inside a project
241
* @:project_menu@ - the main menu displayed when the user is inside a project
242
* @:admin_menu@ - the menu displayed on the Administration page (can only insert after Settings, before Plugins)
243
244
Available options are:
245
246 1 Jean-Philippe Lang
* @:param@ - the parameter key that is used for the project id (default is @:id@)
247 36 Jérémie Delaitre
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
248
* @:caption@ - the menu caption that can be:
249 4 Jean-Philippe Lang
250
  * a localized string Symbol
251
  * a String
252
  * a Proc that can take the project as argument
253 29 Vinod Singh
254 4 Jean-Philippe Lang
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
255 18 Jean-Philippe Lang
* @:first@, @:last@ - if set to true, the item will stay at the beginning/end of the menu (eg. @:last => true@)
256 4 Jean-Philippe Lang
* @:html@ - a hash of html options that are passed to @link_to@ when rendering the menu item
257
258 1 Jean-Philippe Lang
In our example, we've added an item to the application menu which is emtpy by default.
259 19 Jean-Philippe Lang
Restart the application and go to http://localhost:3000:
260 6 Jean-Philippe Lang
261 71 Jean-Philippe Lang
p=. !application_menu.png!
262 6 Jean-Philippe Lang
263 18 Jean-Philippe Lang
Now you can access the polls by clicking the Polls tab from the welcome screen.
264 6 Jean-Philippe Lang
265
h3. Extending the project menu
266 51 Igor Zubkov
267 18 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.
268 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
269
270
<pre><code class="ruby">
271 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
272
  [...]
273 6 Jean-Philippe Lang
274
  permission :polls, { :polls => [:index, :vote] }, :public => true
275
  menu :project_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
276 1 Jean-Philippe Lang
end
277
</code></pre>
278 6 Jean-Philippe Lang
279 67 Jean-Philippe Lang
The second line adds our Polls tab to the project menu, just after the activity tab. 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. Restart the application again and go to one of your projects:
280 6 Jean-Philippe Lang
281 71 Jean-Philippe Lang
p=. !project_menu.png!
282 6 Jean-Philippe Lang
283 67 Jean-Philippe Lang
If you click the Polls tab (in 3rd position), you should notice that the project menu is no longer displayed.
284 19 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
285 6 Jean-Philippe Lang
286 61 Harry Garrood
Edit your PollsController to do so:
287
288 6 Jean-Philippe Lang
<pre><code class="ruby">
289
def index
290 18 Jean-Philippe Lang
  @project = Project.find(params[:project_id])
291 6 Jean-Philippe Lang
  @polls = Poll.find(:all) # @project.polls
292 39 Ric Turley
end
293 4 Jean-Philippe Lang
</code></pre>
294 1 Jean-Philippe Lang
295 4 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.
296 18 Jean-Philippe Lang
297
Now, you should see the project menu when viewing the polls:
298 10 Jean-Philippe Lang
299 71 Jean-Philippe Lang
p=. !project_menu_pools.png!
300 10 Jean-Philippe Lang
301 103 luigifab !
h3. Removing item in menu
302
303
To remove an item in a menu, you can use @delete_menu_item@ like in this example:
304
305
<pre><code class="ruby">
306
Redmine::Plugin.register :redmine_polls do
307
  [...]
308
309
  delete_menu_item :top_menu, :my_page
310
  delete_menu_item :top_menu, :help
311
  delete_menu_item :project_menu, :overview
312
  delete_menu_item :project_menu, :activity
313
  delete_menu_item :project_menu, :news
314
end
315
</code></pre>
316
317 10 Jean-Philippe Lang
h2. Adding new permissions
318 20 Jean-Philippe Lang
319 18 Jean-Philippe Lang
For now, anyone can vote for polls. Let's make it more configurable by changing the permission declaration.
320
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).
321 1 Jean-Philippe Lang
322 67 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ to replace the previous permission declaration with these 2 lines:
323 10 Jean-Philippe Lang
324
<pre><code class="ruby">
325 1 Jean-Philippe Lang
  permission :view_polls, :polls => :index
326 29 Vinod Singh
  permission :vote_polls, :polls => :vote
327 1 Jean-Philippe Lang
</code></pre>
328 10 Jean-Philippe Lang
329 89 Robert Schneider
Restart the application and go to http://localhost:3000/roles/permissions:
330 18 Jean-Philippe Lang
331 71 Jean-Philippe Lang
p=. !permissions1.png!
332 10 Jean-Philippe Lang
333
You're now able to give these permissions to your existing roles.
334 1 Jean-Philippe Lang
335 67 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. 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.
336 18 Jean-Philippe Lang
337 10 Jean-Philippe Lang
Here is how it would look like for the @#index@ action:
338
339
<pre><code class="ruby">
340
class PollsController < ApplicationController
341
  unloadable
342
  
343
  before_filter :find_project, :authorize, :only => :index
344 19 Jean-Philippe Lang
345 10 Jean-Philippe Lang
  [...]
346
  
347
  def index
348
    @polls = Poll.find(:all) # @project.polls
349
  end
350
351
  [...]
352
  
353
  private
354
  
355
  def find_project
356 18 Jean-Philippe Lang
    # @project variable must be set before calling the authorize filter
357 1 Jean-Philippe Lang
    @project = Project.find(params[:project_id])
358
  end
359 4 Jean-Philippe Lang
end
360 1 Jean-Philippe Lang
</code></pre>
361 31 Markus Bockman
362 1 Jean-Philippe Lang
Retrieving the current project before the @#vote@ action could be done using a similar way.
363 37 Randy Syring
After this, viewing and voting polls will be only available to admin users or users that have the appropriate role on the project.
364 31 Markus Bockman
365
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.
366
Simply create an *.yml (eg. @en.yml@) file in @plugins/polls/config/locales@ and fill it with labels like this:
367 67 Jean-Philippe Lang
368 83 Denis Savitskiy
<pre><code class="yaml">
369 86 Lennart Nordgreen
"en":
370 31 Markus Bockman
  permission_view_polls: View Polls
371
  permission_vote_polls: Vote Polls
372 83 Denis Savitskiy
</code></pre>
373 31 Markus Bockman
374 67 Jean-Philippe Lang
In this example the created file is known as @en.yml@, but all other supported language files are also possible too.
375 31 Markus Bockman
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. 
376
377 4 Jean-Philippe Lang
Restart your application and point the permission section.
378
379 56 Thomas Winkel
h2. Creating a project module
380 26 Eric Davis
381 11 Jean-Philippe Lang
For now, the poll functionality is added to all your projects. But you may want to enable polls for some projects only.
382
So, let's create a 'Polls' project module. This is done by wrapping the permissions declaration inside a call to @#project_module@.
383
384
Edit @init.rb@ and change the permissions declaration:
385 18 Jean-Philippe Lang
386
<pre><code class="ruby">
387
  project_module :polls do
388 11 Jean-Philippe Lang
    permission :view_polls, :polls => :index
389 1 Jean-Philippe Lang
    permission :vote_polls, :polls => :vote
390 11 Jean-Philippe Lang
  end
391
</code></pre>
392 18 Jean-Philippe Lang
393 11 Jean-Philippe Lang
Restart the application and go to one of your project settings.
394 29 Vinod Singh
Click on the Modules tab. You should see the Polls module at the end of the modules list (disabled by default):
395 11 Jean-Philippe Lang
396 71 Jean-Philippe Lang
p=. !modules.png!
397 11 Jean-Philippe Lang
398
You can now enable/disable polls at project level.
399
400 16 Jean-Philippe Lang
h2. Improving the plugin views
401
402
h3. Adding stylesheets
403 26 Eric Davis
404 16 Jean-Philippe Lang
Let's start by adding a stylesheet to our plugin views.
405 67 Jean-Philippe Lang
Create a file named @voting.css@ in the @plugins/polls/assets/stylesheets@ directory:
406 16 Jean-Philippe Lang
407 83 Denis Savitskiy
<pre><code class="css">
408 16 Jean-Philippe Lang
a.vote { font-size: 120%; }
409
a.vote.yes { color: green; }
410
a.vote.no  { color: red; }
411 83 Denis Savitskiy
</code></pre>
412 16 Jean-Philippe Lang
413 67 Jean-Philippe Lang
When starting the application, plugin assets are automatically copied to @public/plugin_assets/polls/@ to make them available through your web server. So any change to your plugin stylesheets or javascripts needs an application restart.
414 16 Jean-Philippe Lang
415 90 Robert Schneider
The introduced classes need to be used by the links. So change in file @plugins/polls/app/views/polls/index.html.erb@ the link declarations to:
416
417
<pre><code class="erb">
418
<%= link_to 'Yes', {:action => 'vote', :id => poll[:id], :answer => 'yes' }, :method => :post, :class => 'vote yes' %> (<%= poll.yes %>)
419
<%= link_to 'No', {:action => 'vote', :id => poll[:id], :answer => 'no' }, :method => :post, :class => 'vote no' %> (<%= poll.no %>)
420
</code></pre>
421
422
Then, append the following lines at the end of @index.html.erb@ so that your stylesheet get included in the page header by Redmine:
423 16 Jean-Philippe Lang
424 84 Mischa The Evil
<pre><code class="erb">
425 16 Jean-Philippe Lang
<% content_for :header_tags do %>
426 73 Jean-Philippe Lang
    <%= stylesheet_link_tag 'voting', :plugin => 'polls' %>
427 16 Jean-Philippe Lang
<% end %>
428 83 Denis Savitskiy
</code></pre>
429 16 Jean-Philippe Lang
430 73 Jean-Philippe Lang
Note that the @:plugin => 'polls'@ option is required when calling the @stylesheet_link_tag@ helper.
431 16 Jean-Philippe Lang
432
Javascripts can be included in plugin views using the @javascript_include_tag@ helper in the same way.
433 1 Jean-Philippe Lang
434 16 Jean-Philippe Lang
h3. Setting page title
435 1 Jean-Philippe Lang
436 16 Jean-Philippe Lang
You can set the HTML title from inside your views by using the @html_title@ helper.
437 1 Jean-Philippe Lang
Example:
438 16 Jean-Philippe Lang
439 84 Mischa The Evil
<pre><code class="erb">
440 34 Tom Bostelmann
  <% html_title "Polls" %>
441 83 Denis Savitskiy
</code></pre>
442 34 Tom Bostelmann
443 75 Jean-Philippe Lang
h2. Using hooks
444
445 76 Jean-Philippe Lang
h3. Hooks in views
446
447
Hooks in Redmine views lets you insert custom content to regular Redmine views. For example, looking at source:tags/2.0.0/app/views/projects/show.html.erb#L52 shows that there are 2 hooks available: one named @:view_projects_show_left@ for adding content to the left part and one named @:view_projects_show_right@ for adding content to the right part of the view.
448
449 91 Robert Schneider
To use one or more hooks in views, you need to create a class that inherits from @Redmine::Hook::ViewListener@ and implement methods named with the hook(s) you want to use. To append some content to the project overview, add a class to your plugin and require it in your @init.rb@, then implement methods whose name match the hook names.
450 76 Jean-Philippe Lang
451 92 Robert Schneider
For our plugin create a file @plugins/polls/lib/polls_hook_listener.rb@ with this content:
452 76 Jean-Philippe Lang
453
<pre><code class="ruby">
454 1 Jean-Philippe Lang
class PollsHookListener < Redmine::Hook::ViewListener
455 91 Robert Schneider
  def view_projects_show_left(context = {})
456 76 Jean-Philippe Lang
    return content_tag("p", "Custom content added to the left")
457 1 Jean-Philippe Lang
  end
458 76 Jean-Philippe Lang
459 91 Robert Schneider
  def view_projects_show_right(context = {})
460 1 Jean-Philippe Lang
    return content_tag("p", "Custom content added to the right")
461
  end
462
end
463
</code></pre>
464
465 91 Robert Schneider
Prepend this line to @plugins/polls/init.rb@:
466
467
<pre><code class="ruby">
468 92 Robert Schneider
require_dependency 'polls_hook_listener'
469 91 Robert Schneider
</code></pre>
470
471 92 Robert Schneider
Restart Redmine and have a look into the overview tab of a project. You should see the strings on the left and the right side in the overview.
472 76 Jean-Philippe Lang
473 92 Robert Schneider
You can also use the @render_on@ helper to render a partial. In our plugin you have to replace the just created content in @plugins/polls/lib/polls_hook_listener.rb@ with:
474 76 Jean-Philippe Lang
475 1 Jean-Philippe Lang
<pre><code class="ruby">
476 76 Jean-Philippe Lang
class PollsHookListener < Redmine::Hook::ViewListener
477
  render_on :view_projects_show_left, :partial => "polls/project_overview"
478
end
479
</code></pre>
480
481 93 Robert Schneider
Add the partial to your plugin by creating the file @app/views/polls/_project_overview.html.erb@. Its content (use some text like 'Message from Hook!') will be appended to the left part of the project overview. Don't forget to restart Redmine.
482 76 Jean-Philippe Lang
483
h3. Hooks in controllers
484
485 75 Jean-Philippe Lang
TODO
486
487
h2. Making your plugin configurable
488
489 81 Paul Kerr
Each plugin registered with Redmine is displayed on the admin/plugins page. Support for a basic configuration mechanism is supplied by the Settings controller. This feature is enabled by adding the "settings" method to the plugin registration block in a plugin's init.rb file.
490
491
<pre><code class="ruby">
492
Redmine::Plugin.register :redmine_polls do
493
  [ ... ]
494
495
  settings :default => {'empty' => true}, :partial => 'settings/poll_settings'
496
end
497
</code></pre>
498
499
Adding this will accomplish two things. First, it will add a "Configure" link to the description block for the plugin in the admin/plugins list. Following this link will cause a common plugin configuration template view to be loaded which will in turn render the partial view referenced by :partial. Calling the settings method will also add support in the Setting module for the plugin. The Setting model will store and retrieve a serialized hash based on the plugin name. This hash is accessed using the Setting method name in the form plugin_<plugin name>. For this example, the hash can be accessed by calling Setting.plugin_redmine_polls.
500
501
p=. !plugin_with_config.png!
502
503
The view referenced by the :partial hash key passed to the settings method will be loaded as a partial within the plugin configuration view. The basic page layout is constrained by the plugin configuration view: a form is declared and the submit button is generated. The partial is pulled into the view inside a table div inside the form. Configuration settings for the plugin will be displayed and can be modified via standard HTML form elements.
504
505
p=. !plugin_config_view.png!
506
507 85 Jean-Baptiste Barth
*NB* : if two plugins have the same partial name for settings, the first will override the second's settings page. So be sure you give a unique name to your settings partial.
508
509 81 Paul Kerr
When the page is submitted, the settings_controller will take the parameter hash referenced by 'settings' and store it directly in a serialized format in Setting.plugin_redmine_polls. Each time the page is generated the current value of Setting.plugin_redmine_polls will be assigned to the local variable settings.
510
511
<pre><code class="erb">
512
<table>
513
  <tbody>
514
    <tr>
515
      <th>Notification Default Address</th>
516 98 Denis Savitskiy
      <td>
517
        <input type="text" id="settings_notification_default"
518
                           value="<%= settings['notification_default'] %>"
519
	                       name="settings[notification_default]" >
520
      </td>
521 81 Paul Kerr
    </tr>
522
  </tbody>
523
</table>
524
</code></pre>
525
526
In the example above, the configuration form was not created using Rails form helpers. This is because there is no @settings model but only the setting hash. Form helpers will attempt to access attributes using model accessor methods which do not exist. For example, a call to @settings.notification_default will fail. The value set by this form is accessed as Setting.plugin_redmine_polls['notification_default'].
527
528
Finally, the :default in the settings method call is to register a value that will be returned from the Setting.plugin_redmine_polls call if nothing has been stored in the settings table for this plugin.
529 75 Jean-Philippe Lang
530 34 Tom Bostelmann
h2. Testing your plugin
531
532 71 Jean-Philippe Lang
h3. test/test_helper.rb
533 34 Tom Bostelmann
534
Here are the contents of my test helper file:
535
536 1 Jean-Philippe Lang
<pre>
537 69 Jean-Philippe Lang
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
538 34 Tom Bostelmann
</pre>
539
540 70 Jean-Philippe Lang
h3. Sample test
541 34 Tom Bostelmann
542 70 Jean-Philippe Lang
Contents of @polls_controller_test.rb@:
543 1 Jean-Philippe Lang
544
<pre><code class="ruby">
545 69 Jean-Philippe Lang
require File.expand_path('../../test_helper', __FILE__)
546 34 Tom Bostelmann
547 69 Jean-Philippe Lang
class PollsControllerTest < ActionController::TestCase
548
  fixtures :projects
549 34 Tom Bostelmann
550 69 Jean-Philippe Lang
  def test_index
551
    get :index, :project_id => 1
552 34 Tom Bostelmann
553 88 Niklaus Giger
    assert_response :success
554 69 Jean-Philippe Lang
    assert_template 'index'
555 34 Tom Bostelmann
  end
556 52 Igor Zubkov
end
557 54 Igor Zubkov
</code></pre>
558 34 Tom Bostelmann
559 70 Jean-Philippe Lang
h3. Running test
560 34 Tom Bostelmann
561 70 Jean-Philippe Lang
Initialize the test database if necessary:
562 68 Jean-Philippe Lang
563 34 Tom Bostelmann
<pre>
564 87 Vincent Robert
$ rake db:drop db:create db:migrate redmine:plugins:migrate redmine:load_default_data RAILS_ENV=test
565 48 Igor Zubkov
</pre>
566 34 Tom Bostelmann
567 69 Jean-Philippe Lang
To execute the polls_controller_test.rb:
568 34 Tom Bostelmann
569
<pre>
570 100 Toshi MARUYAMA
$ bundle exec ruby plugins\polls\test\functionals\polls_controller_test.rb
571 47 Mo Morsi
</pre>
572 35 Tom Bostelmann
573
h3. Testing with permissions
574
575 1 Jean-Philippe Lang
If your plugin requires membership to a project, add the following to the beginning of your functional tests:
576 47 Mo Morsi
577 82 Denis Savitskiy
<pre><code class="ruby">
578 47 Mo Morsi
def test_index
579 1 Jean-Philippe Lang
  @request.session[:user_id] = 2
580 47 Mo Morsi
  ...
581 1 Jean-Philippe Lang
end
582 82 Denis Savitskiy
</code></pre>
583 47 Mo Morsi
584
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):
585 1 Jean-Philippe Lang
586 82 Denis Savitskiy
<pre><code class="ruby">
587 47 Mo Morsi
def test_index
588 1 Jean-Philippe Lang
  Role.find(1).add_permission! :my_permission
589
  ...
590 35 Tom Bostelmann
end
591 82 Denis Savitskiy
</code></pre>
592 47 Mo Morsi
593
594
You may enable/disable a specific module like so:
595
596 82 Denis Savitskiy
<pre><code class="ruby">
597 47 Mo Morsi
def test_index
598
  Project.find(1).enabled_module_names = [:mymodule]
599
  ...
600 1 Jean-Philippe Lang
end
601 82 Denis Savitskiy
</code></pre>
602 94 @ go2null
603 95 @ go2null
h3. Reference file hierarchy
604 94 @ go2null
605 96 @ go2null
Here is a simple list of all the files and directories mentioned in this Tutorial and [[Hooks]].  This is useful to ensure standard paths are used and also useful for newbies to know here files should go.
606 94 @ go2null
607
<pre>
608
plugins/PLUGIN/README.rdoc
609
plugins/PLUGIN/init.rb
610
plugins/PLUGIN/app/
611
plugins/PLUGIN/app/controllers/
612 95 @ go2null
plugins/PLUGIN/app/controllers/CONTROLLER_controller.rb
613 94 @ go2null
plugins/PLUGIN/app/helpers/
614 96 @ go2null
plugins/PLUGIN/app/helpers/CONTROLLER_helper.rb
615 94 @ go2null
plugins/PLUGIN/app/models/
616
plugins/PLUGIN/app/models/MODEL.rb
617
plugins/PLUGIN/app/views/
618 95 @ go2null
plugins/PLUGIN/app/views/CONTROLLER/
619 1 Jean-Philippe Lang
plugins/PLUGIN/app/views/CONTROLLER/_PARTIAL.html.erb
620
plugins/PLUGIN/app/views/CONTROLLER/CONTROLLER-ACTION.html.erb
621 96 @ go2null
plugins/PLUGIN/app/views/hooks/
622
plugins/PLUGIN/app/views/hooks/_HOOK.html.erb
623 94 @ go2null
plugins/PLUGIN/app/views/settings/
624
plugins/PLUGIN/app/views/settings/_MODEL_settings.html.erb
625
plugins/PLUGIN/assets/
626
plugins/PLUGIN/assets/images/
627
plugins/PLUGIN/assets/javascripts/
628
plugins/PLUGIN/assets/stylesheets/
629
plugins/PLUGIN/assets/stylesheets/voting.css
630
plugins/PLUGIN/config/
631
plugins/PLUGIN/config/locales/
632
plugins/PLUGIN/config/locales/en.yml
633 1 Jean-Philippe Lang
plugins/PLUGIN/config/routes.rb
634 94 @ go2null
plugins/PLUGIN/db/
635
plugins/PLUGIN/db/migrate/
636 96 @ go2null
plugins/PLUGIN/db/migrate/001_create_MODELs.rb
637 1 Jean-Philippe Lang
plugins/PLUGIN/lib/
638 94 @ go2null
plugins/PLUGIN/lib/PLUGIN_hook_listener.rb
639 96 @ go2null
plugins/PLUGIN/lib/PLUGIN/
640
plugins/PLUGIN/lib/PLUGIN/hooks.rb
641 97 @ go2null
plugins/PLUGIN/lib/PLUGIN/MODEL_patch.rb
642 1 Jean-Philippe Lang
plugins/PLUGIN/lib/tasks/
643 94 @ go2null
plugins/PLUGIN/test/
644
plugins/PLUGIN/test/test_helper.rb
645
plugins/PLUGIN/test/functional/
646 96 @ go2null
plugins/PLUGIN/test/functional/CONTROLLER_controller_test.rb
647 94 @ go2null
plugins/PLUGIN/test/unit/
648
plugins/PLUGIN/test/unit/MODEL_test.rb
649
</pre>