Plugin Tutorial » History » Version 93

Robert Schneider, 2014-06-02 09:37

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