Project

General

Profile

Plugin Tutorial » History » Version 13

Jean-Philippe Lang, 2008-08-10 19:48

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 3 Jean-Philippe Lang
  % ruby script/generate redmine_plugin Pools
15 1 Jean-Philippe Lang
16
The plugin structure is created in @vendor/plugins/redmine_pools@:
17
18
<pre>
19
      create  vendor/plugins/redmine_pools/app/controllers
20
      create  vendor/plugins/redmine_pools/app/helpers
21
      create  vendor/plugins/redmine_pools/app/models
22
      create  vendor/plugins/redmine_pools/app/views
23
      create  vendor/plugins/redmine_pools/db/migrate
24
      create  vendor/plugins/redmine_pools/lib/tasks
25
      create  vendor/plugins/redmine_pools/assets/images
26
      create  vendor/plugins/redmine_pools/assets/javascripts
27
      create  vendor/plugins/redmine_pools/assets/stylesheets
28
      create  vendor/plugins/redmine_pools/lang
29
      create  vendor/plugins/redmine_pools/README
30
      create  vendor/plugins/redmine_pools/init.rb
31
      create  vendor/plugins/redmine_pools/lang/en.yml
32
</pre>
33
34 8 Jean-Philippe Lang
Edit @vendor/plugins/redmine_pools/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
Redmine::Plugin.register :redmine_pools do
40
  name 'Pools plugin'
41
  author 'John Smith'
42
  description 'A plugin for managing pools'
43
  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
TODO
55
56 1 Jean-Philippe Lang
h2. Generating a controller
57
58
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
59 9 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
60
61
  ruby script/generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]
62
63
So go back to the command prompt and run:
64 3 Jean-Philippe Lang
65
<pre>
66
% ruby script/generate redmine_plugin_controller Pools pools index vote
67
      exists  app/controllers/
68
      exists  app/helpers/
69
      create  app/views/pools
70
      create  test/functional/
71
      create  app/controllers/pools_controller.rb
72
      create  test/functional/pools_controller_test.rb
73
      create  app/helpers/pools_helper.rb
74
      create  app/views/pools/index.html.erb
75
      create  app/views/pools/vote.html.erb
76
</pre>
77
78
A controller @PoolsController@ with 2 actions (@#index@ and @#vote@) is created.
79
80 5 Jean-Philippe Lang
Edit @app/controllers/pools_controller.rb@ in @redmine_pools@ directory to implement these 2 actions.
81 3 Jean-Philippe Lang
82
<pre><code class="ruby">
83
class PoolsController < ApplicationController
84
  unloadable
85
  
86
  @@pools = [ {:id => 1, :title => 'First pool', :question => 'This is the first pool question', :yes => 0, :no => 0},
87
              {:id => 2, :title => 'Second pool', :question => 'This is the second pool question', :yes => 0, :no => 0} ]
88
89
  def index
90 7 Jean-Philippe Lang
    @pools = @@pools # Pool.find(:all)
91 3 Jean-Philippe Lang
  end
92
93
  def vote
94 7 Jean-Philippe Lang
    pool = @@pools.find {|p| p[:id].to_s == params[:id]} # Pool.find(params[:id])
95 3 Jean-Philippe Lang
    # saves the vote
96
    pool[params[:answer].to_sym] += 1
97
    flash[:notice] = 'Vote saved.'
98
    redirect_to :action => 'index'
99
  end
100
end
101 1 Jean-Philippe Lang
</code></pre>
102 5 Jean-Philippe Lang
103
For the sake of this example, we simulate a pool model in our @@@pools@ class variable.
104
We could of course use a ActiveRecord model just like we do it in a regular Rails app.
105 3 Jean-Philippe Lang
106
Then edit @app/views/pools/index.html.erb@ that will display existing pools:
107
108
109
<pre>
110
<h2>Pools</h2>
111
112
<% @pools.each do |pool| %>
113
  <p>
114
  <%= pool[:question] %>?
115
  <%= link_to 'Yes', {:action => 'vote', :id => pool[:id], :answer => 'yes'}, :method => :post %> (<%= pool[:yes] %>) /
116
  <%= link_to 'No', {:action => 'vote', :id => pool[:id], :answer => 'no'}, :method => :post %> (<%= pool[:no] %>)
117
  </p>
118
<% end %>
119
</pre>
120
121
You can remove @vote.html.erb@ since no rendering is done by the corresponding action.
122
123 1 Jean-Philippe Lang
Now, restart the application and point your browser to http://localhost:3000/pools.
124 4 Jean-Philippe Lang
You should see the 2 pools and you should be able to vote for them:
125
126
p=. !pools1.png!
127
128
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.
129
130
h2. Extending menus
131
132
Our controller works fine but users have to know the url to see the pools. Using the Redmine plugin API, you can extend standard menus.
133
So let's add a new item to the application menu.
134
135
h3. Extending the application menu
136
137
Edit @init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
138
139
<pre><code class="ruby">
140
Redmine::Plugin.register :redmine_pools do
141
  [...]
142
  
143
  menu :application_menu, :pools, { :controller => 'pools', :action => 'index' }, :caption => 'Pools'
144
end
145
</code></pre>
146
147
Syntax is:
148
149
  menu(menu_name, item_name, url, options={})
150
151
There are 4 menus that you can extend:
152
153
* @:top_menu@ - the top left menu
154
* @:account_menu@ - the top right menu with sign in/sign out links
155
* @:application_menu@ - the main menu displayed when the user is not inside a project
156
* @:project_menu@ - the main menu displayed when the user is inside a project
157
158
Available options are:
159
160
* @:param@ - the parameter key that is used for the project id (default is @:id@)
161
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
162
* @:caption@ - the menu caption that can be:
163
164
  * a localized string Symbol
165
  * a String
166
  * a Proc that can take the project as argument
167
168
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @:after => :activity@)
169
* @:last@ - if set to true, the item will stay at the end of the menu (eg. @:last => true@)
170
* @:html_options@ - a hash of html options that are passed to @link_to@ when rendering the menu item
171
172
In our example, we've added an item to the application menu which is emtpy by default.
173
Restart the application and go to http://localhost:3000:
174
175
p=. !application_menu.png!
176
177
Now you can access the pools by clicking the Pools tab from the welcome screen.
178
179
h3. Extending the project menu
180
181 6 Jean-Philippe Lang
Now, let's consider that the pools are defined at project level (even if it's not the case in our example pool model). So we would like to add the Pools tab to the project menu instead.
182
Open @init.rb@ and replace the line that was added just before with these 2 lines:
183
184
<pre><code class="ruby">
185
Redmine::Plugin.register :redmine_pools do
186
  [...]
187
188
  permission :pools, {:pools => [:index, :vote]}, :public => true
189
  menu :project_menu, :pools, { :controller => 'pools', :action => 'index' }, :caption => 'Pools', :after => :activity, :param => :project_id
190
end
191
</code></pre>
192
193
The second line adds our Pools tab to the project menu, just after the activity tab.
194
The first line is required and declares that our 2 actions from @PoolsController@ are public. We'll come back later to explain this with more details.
195
196
Restart the application again and go to one of your projects:
197
198
p=. !project_menu.png!
199
200
If you click the Pools tab, you should notice that the project menu is no longer displayed.
201
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
202
203
Edit your PoolsController to do so:
204
205
<pre><code class="ruby">
206
def index
207
  @project = Project.find(params[:project_id])
208 7 Jean-Philippe Lang
  @pools = @@pools # @project.pools
209 6 Jean-Philippe Lang
end
210
</code></pre>
211
212
The project id is available in the @:project_id@ param because of the @:param => :project_id@ option in the menu item declaration above.
213
214
Now, you should see the project menu when viewing the pools:
215
216
p=. !project_menu_pools.png!
217 4 Jean-Philippe Lang
218
h2. Adding new permissions
219
220 10 Jean-Philippe Lang
For now, anyone can vote for pools. Let's make it more configurable by changing the permission declaration.
221
We're going to declare 2 project based permissions, one for viewing the pools and an other one for voting. These permissions are no longer public (@:public => true@ option is removed).
222
223
Edit @init.rb@ to replace the previous permission declaration with these 2 lines:
224
225
<pre><code class="ruby">
226
  permission :view_pools, :pools => :index
227
  permission :vote_pools, :pools => :vote
228
</code></pre>
229
230
Restart the application and go to http://localhost:3000/roles/report:
231
232
p=. !permissions1.png!
233
234
You're now able to give these permissions to your existing roles.
235
236
Of course, some code needs to be added to the PoolsController so that actions are actually protected according to the permissions of the current user.
237
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.
238
239
Here is how it would look like for the @#index@ action:
240
241
<pre><code class="ruby">
242
class PoolsController < ApplicationController
243
  unloadable
244
  
245
  before_filter :find_project, :authorize, :only => :index
246
247
  [...]
248
  
249
  def index
250
    @pools = @@pools # @project.pools
251
  end
252
253
  [...]
254
  
255
  private
256
  
257
  def find_project
258
    # @project variable must be set before calling the authorize filter
259
    @project = Project.find(params[:project_id])
260
  end
261
end
262
</code></pre>
263
264
Retrieving the current project before the @#vote@ action could be done using a similiar way.
265
After this, viewing and voting pools will be only available to admin users or users that have the appropriate role on the project.
266 4 Jean-Philippe Lang
267
h2. Creating a project module
268
269 11 Jean-Philippe Lang
For now, the pool functionality is added to all your projects. But you way want to enable pools for some projects only.
270
So, let's create a 'Pools' project module. This is done by wraping the permissions declaration inside a call to @#project_module@.
271
272
Edit @init.rb@ and change the permissions declaration:
273
274
<pre><code class="ruby">
275
  project_module :pools do
276
    permission :view_pools, :pools => :index
277
    permission :vote_pools, :pools => :vote
278
  end
279
</code></pre>
280
281
Restart the application and go to one of your project settings.
282
Click on the Modules tab. You should see the Pools module at the end of the modules list (disabled by default):
283
284
p=. !modules.png!
285
286
You can now enable/disable pools at project level.
287
288
h2. Improving the plugin views
289
290
TODO: adding stylesheet, setting page title