Plugin Tutorial » History » Version 27

Eduardo Yáñez Parareda, 2009-09-25 10:48
Change the URL where the plugins are showed

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