Project

General

Profile

Plugin Tutorial » History » Version 31

Markus Bockman, 2010-01-11 21:29

1 1 Jean-Philippe Lang
h1. Plugin Tutorial
2 12 Jean-Philippe Lang
3 20 Jean-Philippe Lang
Note: To follow this tutorial, you need to run Redmine devel r1786 or higher.
4
5 30 Vinod Singh
{{>toc}}
6 1 Jean-Philippe Lang
7
h2. Creating a new Plugin
8
9 9 Jean-Philippe Lang
Creating a new plugin can be done using the Redmine plugin generator.
10
Syntax for this generator is:
11 1 Jean-Philippe Lang
12 23 Jean-Baptiste Barth
<pre>ruby script/generate redmine_plugin <plugin_name></pre>
13 9 Jean-Philippe Lang
14
So open up a command prompt and "cd" to your redmine directory, then execute the following command:
15
16 18 Jean-Philippe Lang
  % ruby script/generate redmine_plugin Polls
17 1 Jean-Philippe Lang
18 18 Jean-Philippe Lang
The plugin structure is created in @vendor/plugins/redmine_polls@:
19 1 Jean-Philippe Lang
20
<pre>
21 18 Jean-Philippe Lang
      create  vendor/plugins/redmine_polls/app/controllers
22
      create  vendor/plugins/redmine_polls/app/helpers
23
      create  vendor/plugins/redmine_polls/app/models
24
      create  vendor/plugins/redmine_polls/app/views
25
      create  vendor/plugins/redmine_polls/db/migrate
26
      create  vendor/plugins/redmine_polls/lib/tasks
27
      create  vendor/plugins/redmine_polls/assets/images
28
      create  vendor/plugins/redmine_polls/assets/javascripts
29
      create  vendor/plugins/redmine_polls/assets/stylesheets
30
      create  vendor/plugins/redmine_polls/lang
31
      create  vendor/plugins/redmine_polls/README
32
      create  vendor/plugins/redmine_polls/init.rb
33
      create  vendor/plugins/redmine_polls/lang/en.yml
34 1 Jean-Philippe Lang
</pre>
35
36 18 Jean-Philippe Lang
Edit @vendor/plugins/redmine_polls/init.rb@ to adjust plugin information (name, author, description and version):
37 1 Jean-Philippe Lang
38
<pre><code class="ruby">
39
require 'redmine'
40
41 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
42
  name 'Polls plugin'
43 1 Jean-Philippe Lang
  author 'John Smith'
44 18 Jean-Philippe Lang
  description 'A plugin for managing polls'
45 1 Jean-Philippe Lang
  version '0.0.1'
46
end
47
</code></pre>
48
49 27 Eduardo Yáñez Parareda
Then restart the application and point your browser to http://localhost:3000/admin/plugins.
50 1 Jean-Philippe Lang
After logging in, you should see your new plugin in the plugins list:
51 4 Jean-Philippe Lang
52 29 Vinod Singh
!plugins_list1.png!
53 1 Jean-Philippe Lang
54 13 Jean-Philippe Lang
h2. Generating a model
55
56 19 Jean-Philippe Lang
Let's create a simple Poll model for our plugin:
57 1 Jean-Philippe Lang
58 28 John Fisher
   ruby script/generate redmine_plugin_model polls poll question:string yes:integer no:integer
59 14 Jean-Philippe Lang
60 19 Jean-Philippe Lang
This creates the Poll model and the corresponding migration file.
61 1 Jean-Philippe Lang
62 28 John Fisher
*Please note you may have to rename your migration.* Timestamped migrations are not supported by the actual Redmine plugin engine (Engines). If your migrations are named with a timestamp, rename it using "001", "002", etc. instead.
63
64
   <pre>cd redmine/vendor/plugins/redmine_polls/db/migrate
65
mv  20091009211553_create_polls.rb 001_create_polls.rb</pre>
66
67
If you have already created a database table record in plugin_schema_info with the timestamp version number, you will have to change it to reflect your new version number, or the migration will hang.
68
69 21 Jean-Baptiste Barth
70 14 Jean-Philippe Lang
Migrate the database using the following command:
71
72
  rake db:migrate_plugins
73
74
Note that each plugin has its own set of migrations.
75
76 24 Eric Davis
Lets add some Polls in the console so we have something to work with.  The console is where you an interactively work and examine the Redmine environment and is very informative to play around in.  But for now we just need create two Poll objects
77
78
<pre>
79
script/console
80
>> Poll.create(:question => "Can you see this poll ?")
81
>> Poll.create(:question => "And can you see this other poll ?")
82
>> exit
83
</pre>
84
85 26 Eric Davis
Edit @vendor/plugins/redmine_polls/app/models/poll.rb@ in your plugin directory to add a #vote method that will be invoked from our controller:
86 15 Jean-Philippe Lang
87
<pre><code class="ruby">
88 19 Jean-Philippe Lang
class Poll < ActiveRecord::Base
89 15 Jean-Philippe Lang
  def vote(answer)
90
    increment(answer == 'yes' ? :yes : :no)
91
  end
92
end
93
</code></pre>
94
95 1 Jean-Philippe Lang
h2. Generating a controller
96
97
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
98 9 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
99
100 23 Jean-Baptiste Barth
<pre>ruby script/generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]</pre>
101 9 Jean-Philippe Lang
102
So go back to the command prompt and run:
103 3 Jean-Philippe Lang
104
<pre>
105 18 Jean-Philippe Lang
% ruby script/generate redmine_plugin_controller Polls polls index vote
106 3 Jean-Philippe Lang
      exists  app/controllers/
107
      exists  app/helpers/
108 18 Jean-Philippe Lang
      create  app/views/polls
109 3 Jean-Philippe Lang
      create  test/functional/
110 18 Jean-Philippe Lang
      create  app/controllers/polls_controller.rb
111
      create  test/functional/polls_controller_test.rb
112
      create  app/helpers/polls_helper.rb
113
      create  app/views/polls/index.html.erb
114
      create  app/views/polls/vote.html.erb
115 3 Jean-Philippe Lang
</pre>
116
117 18 Jean-Philippe Lang
A controller @PollsController@ with 2 actions (@#index@ and @#vote@) is created.
118 3 Jean-Philippe Lang
119 26 Eric Davis
Edit @vendor/plugins/redmine_polls/app/controllers/polls_controller.rb@ in @redmine_polls@ directory to implement these 2 actions.
120 3 Jean-Philippe Lang
121
<pre><code class="ruby">
122 18 Jean-Philippe Lang
class PollsController < ApplicationController
123 1 Jean-Philippe Lang
  unloadable
124
125 7 Jean-Philippe Lang
  def index
126 19 Jean-Philippe Lang
    @polls = Poll.find(:all)
127 3 Jean-Philippe Lang
  end
128 7 Jean-Philippe Lang
129 19 Jean-Philippe Lang
  def vote
130 1 Jean-Philippe Lang
    poll = Poll.find(params[:id])
131 21 Jean-Baptiste Barth
    poll.vote(params[:answer])
132 25 Eric Davis
    if poll.save
133
      flash[:notice] = 'Vote saved.'
134
      redirect_to :action => 'index'
135
    end
136 3 Jean-Philippe Lang
  end
137
end
138 1 Jean-Philippe Lang
</code></pre>
139 5 Jean-Philippe Lang
140 26 Eric Davis
Then edit @vendor/plugins/redmine_polls/app/views/polls/index.html.erb@ that will display existing polls:
141 3 Jean-Philippe Lang
142
143
<pre>
144 18 Jean-Philippe Lang
<h2>Polls</h2>
145 3 Jean-Philippe Lang
146 19 Jean-Philippe Lang
<% @polls.each do |poll| %>
147 3 Jean-Philippe Lang
  <p>
148 19 Jean-Philippe Lang
  <%= poll[:question] %>?
149
  <%= link_to 'Yes', {:action => 'vote', :id => poll[:id], :answer => 'yes'}, :method => :post %> (<%= poll[:yes] %>) /
150
  <%= link_to 'No', {:action => 'vote', :id => poll[:id], :answer => 'no'}, :method => :post %> (<%= poll[:no] %>)
151 3 Jean-Philippe Lang
  </p>
152
<% end %>
153
</pre>
154
155 26 Eric Davis
You can remove @vendor/plugins/redmine_polls/app/views/polls/vote.html.erb@ since no rendering is done by the corresponding action.
156 3 Jean-Philippe Lang
157 18 Jean-Philippe Lang
Now, restart the application and point your browser to http://localhost:3000/polls.
158
You should see the 2 polls and you should be able to vote for them:
159 4 Jean-Philippe Lang
160 29 Vinod Singh
!pools1.png!
161 4 Jean-Philippe Lang
162 19 Jean-Philippe Lang
Note that poll results are reset on each request if you don't run the application in production mode, since our poll "model" is stored in a class variable in this example.
163 4 Jean-Philippe Lang
164
h2. Extending menus
165
166 18 Jean-Philippe Lang
Our controller works fine but users have to know the url to see the polls. Using the Redmine plugin API, you can extend standard menus.
167 4 Jean-Philippe Lang
So let's add a new item to the application menu.
168
169
h3. Extending the application menu
170
171 26 Eric Davis
Edit @vendor/plugins/redmine_polls/init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
172 4 Jean-Philippe Lang
173
<pre><code class="ruby">
174 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
175 4 Jean-Philippe Lang
  [...]
176
  
177 18 Jean-Philippe Lang
  menu :application_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls'
178 4 Jean-Philippe Lang
end
179
</code></pre>
180
181
Syntax is:
182
183
  menu(menu_name, item_name, url, options={})
184
185
There are 4 menus that you can extend:
186
187
* @:top_menu@ - the top left menu
188
* @:account_menu@ - the top right menu with sign in/sign out links
189
* @:application_menu@ - the main menu displayed when the user is not inside a project
190
* @:project_menu@ - the main menu displayed when the user is inside a project
191
192
Available options are:
193
194
* @:param@ - the parameter key that is used for the project id (default is @:id@)
195
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
196
* @:caption@ - the menu caption that can be:
197
198
  * a localized string Symbol
199
  * a String
200
  * a Proc that can take the project as argument
201
202
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
203
* @:last@ - if set to true, the item will stay at the end of the menu (eg. @:last => true@)
204
* @:html_options@ - a hash of html options that are passed to @link_to@ when rendering the menu item
205
206
In our example, we've added an item to the application menu which is emtpy by default.
207
Restart the application and go to http://localhost:3000:
208
209 29 Vinod Singh
!application_menu.png!
210 4 Jean-Philippe Lang
211 18 Jean-Philippe Lang
Now you can access the polls by clicking the Polls tab from the welcome screen.
212 4 Jean-Philippe Lang
213
h3. Extending the project menu
214
215 19 Jean-Philippe Lang
Now, let's consider that the polls are defined at project level (even if it's not the case in our example poll model). So we would like to add the Polls tab to the project menu instead.
216 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
217
218
<pre><code class="ruby">
219 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
220 6 Jean-Philippe Lang
  [...]
221
222 18 Jean-Philippe Lang
  permission :polls, {:polls => [:index, :vote]}, :public => true
223
  menu :project_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
224 6 Jean-Philippe Lang
end
225
</code></pre>
226
227 18 Jean-Philippe Lang
The second line adds our Polls tab to the project menu, just after the activity tab.
228
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.
229 6 Jean-Philippe Lang
230
Restart the application again and go to one of your projects:
231
232 29 Vinod Singh
!project_menu.png!
233 6 Jean-Philippe Lang
234 18 Jean-Philippe Lang
If you click the Polls tab, you should notice that the project menu is no longer displayed.
235 6 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
236
237 18 Jean-Philippe Lang
Edit your PollsController to do so:
238 6 Jean-Philippe Lang
239
<pre><code class="ruby">
240
def index
241
  @project = Project.find(params[:project_id])
242 19 Jean-Philippe Lang
  @polls = Poll.find(:all) # @project.polls
243 6 Jean-Philippe Lang
end
244
</code></pre>
245
246
The project id is available in the @:project_id@ param because of the @:param => :project_id@ option in the menu item declaration above.
247
248 18 Jean-Philippe Lang
Now, you should see the project menu when viewing the polls:
249 6 Jean-Philippe Lang
250 29 Vinod Singh
!project_menu_pools.png!
251 4 Jean-Philippe Lang
252
h2. Adding new permissions
253
254 18 Jean-Philippe Lang
For now, anyone can vote for polls. Let's make it more configurable by changing the permission declaration.
255
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).
256 10 Jean-Philippe Lang
257 26 Eric Davis
Edit @vendor/plugins/redmine_polls/init.rb@ to replace the previous permission declaration with these 2 lines:
258 10 Jean-Philippe Lang
259
<pre><code class="ruby">
260 20 Jean-Philippe Lang
261 18 Jean-Philippe Lang
  permission :view_polls, :polls => :index
262
  permission :vote_polls, :polls => :vote
263 1 Jean-Philippe Lang
</code></pre>
264 14 Jean-Philippe Lang
265 10 Jean-Philippe Lang
266
Restart the application and go to http://localhost:3000/roles/report:
267
268 29 Vinod Singh
!permissions1.png!
269 10 Jean-Philippe Lang
270
You're now able to give these permissions to your existing roles.
271
272 18 Jean-Philippe Lang
Of course, some code needs to be added to the PollsController so that actions are actually protected according to the permissions of the current user.
273 10 Jean-Philippe Lang
For this, we just need to append the @:authorize@ filter and make sure that the @project instance variable is properly set before calling this filter.
274
275
Here is how it would look like for the @#index@ action:
276
277 1 Jean-Philippe Lang
<pre><code class="ruby">
278 18 Jean-Philippe Lang
class PollsController < ApplicationController
279 10 Jean-Philippe Lang
  unloadable
280
  
281
  before_filter :find_project, :authorize, :only => :index
282
283
  [...]
284
  
285
  def index
286 19 Jean-Philippe Lang
    @polls = Poll.find(:all) # @project.polls
287 10 Jean-Philippe Lang
  end
288
289
  [...]
290
  
291
  private
292
  
293
  def find_project
294
    # @project variable must be set before calling the authorize filter
295
    @project = Project.find(params[:project_id])
296
  end
297
end
298
</code></pre>
299
300 26 Eric Davis
Retrieving the current project before the @#vote@ action could be done using a similar way.
301 18 Jean-Philippe Lang
After this, viewing and voting polls will be only available to admin users or users that have the appropriate role on the project.
302 4 Jean-Philippe Lang
303 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.
304
Simply create an *.yml file in the @vendor/plugins/redmine_polls/lang@ directory of your plugin and fill it with labels like this:
305
306
<pre><code class="ruby">
307
308
  permission_view_polls: View Polls
309
  permission_vote_polls: Vote Polls
310
311
</code></pre>
312
313
In this example the created file is known as en.yml, but all other supported language files are also possible too.
314
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. 
315
316
Restart your application and point the permission section.
317
318 4 Jean-Philippe Lang
h2. Creating a project module
319
320 19 Jean-Philippe Lang
For now, the poll functionality is added to all your projects. But you way want to enable polls for some projects only.
321 26 Eric Davis
So, let's create a 'Polls' project module. This is done by wrapping the permissions declaration inside a call to @#project_module@.
322 11 Jean-Philippe Lang
323
Edit @init.rb@ and change the permissions declaration:
324
325
<pre><code class="ruby">
326 18 Jean-Philippe Lang
  project_module :polls do
327
    permission :view_polls, :polls => :index
328
    permission :vote_polls, :polls => :vote
329 11 Jean-Philippe Lang
  end
330
</code></pre>
331
332
Restart the application and go to one of your project settings.
333 18 Jean-Philippe Lang
Click on the Modules tab. You should see the Polls module at the end of the modules list (disabled by default):
334 11 Jean-Philippe Lang
335 29 Vinod Singh
!modules.png!
336 11 Jean-Philippe Lang
337 18 Jean-Philippe Lang
You can now enable/disable polls at project level.
338 11 Jean-Philippe Lang
339
h2. Improving the plugin views
340
341 16 Jean-Philippe Lang
h3. Adding stylesheets
342
343
Let's start by adding a stylesheet to our plugin views.
344 26 Eric Davis
Create a file named @voting.css@ in the @vendor/plugins/redmine_polls/assets/stylesheets@ directory:
345 16 Jean-Philippe Lang
346
<pre>
347
a.vote { font-size: 120%; }
348
a.vote.yes { color: green; }
349
a.vote.no  { color: red; }
350
</pre>
351
352 18 Jean-Philippe Lang
When starting the application, plugin assets are automatically copied to @public/plugin_assets/redmine_polls/@ by Rails Engines to make them available through your web server. So any change to your plugin stylesheets or javascripts needs an application restart.
353 16 Jean-Philippe Lang
354 26 Eric Davis
Then, append the following lines at the end of @vendor/plugins/redmine_polls/app/views/polls/index.html.erb@ so that your stylesheet get included in the page header by Redmine:
355 16 Jean-Philippe Lang
356
<pre>
357
<% content_for :header_tags do %>
358 18 Jean-Philippe Lang
    <%= stylesheet_link_tag 'voting', :plugin => 'redmine_polls' %>
359 16 Jean-Philippe Lang
<% end %>
360
</pre>
361
362 18 Jean-Philippe Lang
Note that the @:plugin => 'redmine_polls'@ option is required when calling the @stylesheet_link_tag@ helper.
363 16 Jean-Philippe Lang
364
Javascripts can be included in plugin views using the @javascript_include_tag@ helper in the same way.
365
366
h3. Setting page title
367
368
You can set the HTML title from inside your views by using the @html_title@ helper.
369
Example:
370
371 18 Jean-Philippe Lang
  <% html_title "Polls" -%>