Plugin Tutorial » History » Version 104

Go MAEDA, 2018-01-09 10:15
Removed "unloadable" (#20513) and fixed some typos.

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