Project

General

Profile

Plugin Tutorial » History » Version 24

Eric Davis, 2009-03-28 01:45
Added section on creating data using the console

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 12 Jean-Philippe Lang
{{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 2 Jean-Philippe Lang
Then restart the application and point your browser to http://localhost:3000/admin/info.
50 1 Jean-Philippe Lang
After logging in, you should see your new plugin in the plugins list:
51 4 Jean-Philippe Lang
52 2 Jean-Philippe Lang
p=. !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 19 Jean-Philippe Lang
  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 21 Jean-Baptiste Barth
62
Please note that 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 14 Jean-Philippe Lang
Migrate the database using the following command:
65
66
  rake db:migrate_plugins
67
68
Note that each plugin has its own set of migrations.
69
70 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
71
72
<pre>
73
script/console
74
>> Poll.create(:question => "Can you see this poll ?")
75
>> Poll.create(:question => "And can you see this other poll ?")
76
>> exit
77
</pre>
78
79 19 Jean-Philippe Lang
Edit @app/models/poll.rb@ in your plugin directory to add a #vote method that will be invoked from our controller:
80 15 Jean-Philippe Lang
81
<pre><code class="ruby">
82 19 Jean-Philippe Lang
class Poll < ActiveRecord::Base
83 15 Jean-Philippe Lang
  def vote(answer)
84
    increment(answer == 'yes' ? :yes : :no)
85
  end
86
end
87
</code></pre>
88
89 1 Jean-Philippe Lang
h2. Generating a controller
90
91
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
92 9 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
93
94 23 Jean-Baptiste Barth
<pre>ruby script/generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]</pre>
95 9 Jean-Philippe Lang
96
So go back to the command prompt and run:
97 3 Jean-Philippe Lang
98
<pre>
99 18 Jean-Philippe Lang
% ruby script/generate redmine_plugin_controller Polls polls index vote
100 3 Jean-Philippe Lang
      exists  app/controllers/
101
      exists  app/helpers/
102 18 Jean-Philippe Lang
      create  app/views/polls
103 3 Jean-Philippe Lang
      create  test/functional/
104 18 Jean-Philippe Lang
      create  app/controllers/polls_controller.rb
105
      create  test/functional/polls_controller_test.rb
106
      create  app/helpers/polls_helper.rb
107
      create  app/views/polls/index.html.erb
108
      create  app/views/polls/vote.html.erb
109 3 Jean-Philippe Lang
</pre>
110
111 18 Jean-Philippe Lang
A controller @PollsController@ with 2 actions (@#index@ and @#vote@) is created.
112 3 Jean-Philippe Lang
113 18 Jean-Philippe Lang
Edit @app/controllers/polls_controller.rb@ in @redmine_polls@ directory to implement these 2 actions.
114 3 Jean-Philippe Lang
115
<pre><code class="ruby">
116 18 Jean-Philippe Lang
class PollsController < ApplicationController
117 1 Jean-Philippe Lang
  unloadable
118
119 7 Jean-Philippe Lang
  def index
120 19 Jean-Philippe Lang
    @polls = Poll.find(:all)
121 3 Jean-Philippe Lang
  end
122 7 Jean-Philippe Lang
123 19 Jean-Philippe Lang
  def vote
124 1 Jean-Philippe Lang
    poll = Poll.find(params[:id])
125 21 Jean-Baptiste Barth
    poll.vote(params[:answer])
126 3 Jean-Philippe Lang
    flash[:notice] = 'Vote saved.'
127
    redirect_to :action => 'index'
128
  end
129
end
130 1 Jean-Philippe Lang
</code></pre>
131 5 Jean-Philippe Lang
132 18 Jean-Philippe Lang
Then edit @app/views/polls/index.html.erb@ that will display existing polls:
133 3 Jean-Philippe Lang
134
135
<pre>
136 18 Jean-Philippe Lang
<h2>Polls</h2>
137 3 Jean-Philippe Lang
138 19 Jean-Philippe Lang
<% @polls.each do |poll| %>
139 3 Jean-Philippe Lang
  <p>
140 19 Jean-Philippe Lang
  <%= poll[:question] %>?
141
  <%= link_to 'Yes', {:action => 'vote', :id => poll[:id], :answer => 'yes'}, :method => :post %> (<%= poll[:yes] %>) /
142
  <%= link_to 'No', {:action => 'vote', :id => poll[:id], :answer => 'no'}, :method => :post %> (<%= poll[:no] %>)
143 3 Jean-Philippe Lang
  </p>
144
<% end %>
145
</pre>
146
147
You can remove @vote.html.erb@ since no rendering is done by the corresponding action.
148
149 18 Jean-Philippe Lang
Now, restart the application and point your browser to http://localhost:3000/polls.
150
You should see the 2 polls and you should be able to vote for them:
151 4 Jean-Philippe Lang
152 22 Mischa The Evil
p=. !pools1.png!
153 4 Jean-Philippe Lang
154 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.
155 4 Jean-Philippe Lang
156
h2. Extending menus
157
158 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.
159 4 Jean-Philippe Lang
So let's add a new item to the application menu.
160
161
h3. Extending the application menu
162
163
Edit @init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
164
165
<pre><code class="ruby">
166 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
167 4 Jean-Philippe Lang
  [...]
168
  
169 18 Jean-Philippe Lang
  menu :application_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls'
170 4 Jean-Philippe Lang
end
171
</code></pre>
172
173
Syntax is:
174
175
  menu(menu_name, item_name, url, options={})
176
177
There are 4 menus that you can extend:
178
179
* @:top_menu@ - the top left menu
180
* @:account_menu@ - the top right menu with sign in/sign out links
181
* @:application_menu@ - the main menu displayed when the user is not inside a project
182
* @:project_menu@ - the main menu displayed when the user is inside a project
183
184
Available options are:
185
186
* @:param@ - the parameter key that is used for the project id (default is @:id@)
187
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
188
* @:caption@ - the menu caption that can be:
189
190
  * a localized string Symbol
191
  * a String
192
  * a Proc that can take the project as argument
193
194
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
195
* @:last@ - if set to true, the item will stay at the end of the menu (eg. @:last => true@)
196
* @:html_options@ - a hash of html options that are passed to @link_to@ when rendering the menu item
197
198
In our example, we've added an item to the application menu which is emtpy by default.
199
Restart the application and go to http://localhost:3000:
200
201
p=. !application_menu.png!
202
203 18 Jean-Philippe Lang
Now you can access the polls by clicking the Polls tab from the welcome screen.
204 4 Jean-Philippe Lang
205
h3. Extending the project menu
206
207 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.
208 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
209
210
<pre><code class="ruby">
211 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
212 6 Jean-Philippe Lang
  [...]
213
214 18 Jean-Philippe Lang
  permission :polls, {:polls => [:index, :vote]}, :public => true
215
  menu :project_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
216 6 Jean-Philippe Lang
end
217
</code></pre>
218
219 18 Jean-Philippe Lang
The second line adds our Polls tab to the project menu, just after the activity tab.
220
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.
221 6 Jean-Philippe Lang
222
Restart the application again and go to one of your projects:
223
224
p=. !project_menu.png!
225
226 18 Jean-Philippe Lang
If you click the Polls tab, you should notice that the project menu is no longer displayed.
227 6 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
228
229 18 Jean-Philippe Lang
Edit your PollsController to do so:
230 6 Jean-Philippe Lang
231
<pre><code class="ruby">
232
def index
233
  @project = Project.find(params[:project_id])
234 19 Jean-Philippe Lang
  @polls = Poll.find(:all) # @project.polls
235 6 Jean-Philippe Lang
end
236
</code></pre>
237
238
The project id is available in the @:project_id@ param because of the @:param => :project_id@ option in the menu item declaration above.
239
240 18 Jean-Philippe Lang
Now, you should see the project menu when viewing the polls:
241 6 Jean-Philippe Lang
242 22 Mischa The Evil
p=. !project_menu_pools.png!
243 4 Jean-Philippe Lang
244
h2. Adding new permissions
245
246 18 Jean-Philippe Lang
For now, anyone can vote for polls. Let's make it more configurable by changing the permission declaration.
247
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).
248 10 Jean-Philippe Lang
249
Edit @init.rb@ to replace the previous permission declaration with these 2 lines:
250
251
<pre><code class="ruby">
252 20 Jean-Philippe Lang
253 18 Jean-Philippe Lang
  permission :view_polls, :polls => :index
254
  permission :vote_polls, :polls => :vote
255 1 Jean-Philippe Lang
</code></pre>
256 14 Jean-Philippe Lang
257 10 Jean-Philippe Lang
258
Restart the application and go to http://localhost:3000/roles/report:
259
260
p=. !permissions1.png!
261
262
You're now able to give these permissions to your existing roles.
263
264 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.
265 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.
266
267
Here is how it would look like for the @#index@ action:
268
269 1 Jean-Philippe Lang
<pre><code class="ruby">
270 18 Jean-Philippe Lang
class PollsController < ApplicationController
271 10 Jean-Philippe Lang
  unloadable
272
  
273
  before_filter :find_project, :authorize, :only => :index
274
275
  [...]
276
  
277
  def index
278 19 Jean-Philippe Lang
    @polls = Poll.find(:all) # @project.polls
279 10 Jean-Philippe Lang
  end
280
281
  [...]
282
  
283
  private
284
  
285
  def find_project
286
    # @project variable must be set before calling the authorize filter
287
    @project = Project.find(params[:project_id])
288
  end
289
end
290
</code></pre>
291
292
Retrieving the current project before the @#vote@ action could be done using a similiar way.
293 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.
294 4 Jean-Philippe Lang
295
h2. Creating a project module
296
297 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.
298 18 Jean-Philippe Lang
So, let's create a 'Polls' project module. This is done by wraping the permissions declaration inside a call to @#project_module@.
299 11 Jean-Philippe Lang
300
Edit @init.rb@ and change the permissions declaration:
301
302
<pre><code class="ruby">
303 18 Jean-Philippe Lang
  project_module :polls do
304
    permission :view_polls, :polls => :index
305
    permission :vote_polls, :polls => :vote
306 11 Jean-Philippe Lang
  end
307
</code></pre>
308
309
Restart the application and go to one of your project settings.
310 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):
311 11 Jean-Philippe Lang
312
p=. !modules.png!
313
314 18 Jean-Philippe Lang
You can now enable/disable polls at project level.
315 11 Jean-Philippe Lang
316
h2. Improving the plugin views
317
318 16 Jean-Philippe Lang
h3. Adding stylesheets
319
320
Let's start by adding a stylesheet to our plugin views.
321
Create a file named @voting.css@ in the @assets/stylesheets@ directory of your plugin:
322
323
<pre>
324
a.vote { font-size: 120%; }
325
a.vote.yes { color: green; }
326
a.vote.no  { color: red; }
327
</pre>
328
329 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.
330 16 Jean-Philippe Lang
331 18 Jean-Philippe Lang
Then, append the following lines at the end of @app/views/polls/index.html.erb@ so that your stylesheet get included in the page header by Redmine:
332 16 Jean-Philippe Lang
333
<pre>
334
<% content_for :header_tags do %>
335 18 Jean-Philippe Lang
    <%= stylesheet_link_tag 'voting', :plugin => 'redmine_polls' %>
336 16 Jean-Philippe Lang
<% end %>
337
</pre>
338
339 18 Jean-Philippe Lang
Note that the @:plugin => 'redmine_polls'@ option is required when calling the @stylesheet_link_tag@ helper.
340 16 Jean-Philippe Lang
341
Javascripts can be included in plugin views using the @javascript_include_tag@ helper in the same way.
342
343
h3. Setting page title
344
345
You can set the HTML title from inside your views by using the @html_title@ helper.
346
Example:
347
348 18 Jean-Philippe Lang
  <% html_title "Polls" -%>