Project

General

Profile

Plugin Tutorial » History » Version 18

Jean-Philippe Lang, 2008-09-11 17:22
typo

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