Plugin Tutorial » History » Version 110

keineahnung 2345, 2021-01-28 04:47
The original code leads to "Page not found The page you were trying to access doesn't exist or has been removed.", this revision fixes it

1 1 Jean-Philippe Lang
h1. Plugin Tutorial
2 12 Jean-Philippe Lang
3 105 Mizuki ISHIKAWA
This tutorial is based on Redmine 4.x and 3.x.
4 105 Mizuki ISHIKAWA
You can view a previous version of this tutorial.
5 105 Mizuki ISHIKAWA
"Redmine 1.x":/projects/redmine/wiki/Plugin_Tutorial?version=66
6 105 Mizuki ISHIKAWA
"Redmine 2.x":/projects/redmine/wiki/Plugin_Tutorial?version=104
7 105 Mizuki ISHIKAWA
8 99 Toshi MARUYAMA
It assumes that you're familiar with the Ruby on Rails framework.
9 99 Toshi MARUYAMA
10 30 Vinod Singh
{{>toc}}
11 1 Jean-Philippe Lang
12 1 Jean-Philippe Lang
h2. Creating a new Plugin
13 40 Nick Peelman
 
14 40 Nick Peelman
You may need to set the RAILS_ENV variable in order to use the command below:
15 32 Jiří Křivánek
16 32 Jiří Křivánek
<pre>
17 32 Jiří Křivánek
$ export RAILS_ENV="production"
18 32 Jiří Křivánek
</pre>
19 32 Jiří Křivánek
20 59 Harry Garrood
On windows:
21 59 Harry Garrood
22 59 Harry Garrood
<pre>
23 1 Jean-Philippe Lang
$ set RAILS_ENV=production
24 59 Harry Garrood
</pre>
25 59 Harry Garrood
26 9 Jean-Philippe Lang
Creating a new plugin can be done using the Redmine plugin generator.
27 1 Jean-Philippe Lang
Syntax for this generator is:
28 1 Jean-Philippe Lang
29 105 Mizuki ISHIKAWA
<pre>bundle exec rails generate redmine_plugin <plugin_name></pre>
30 9 Jean-Philippe Lang
31 1 Jean-Philippe Lang
So open up a command prompt and "cd" to your redmine directory, then execute the following command:
32 1 Jean-Philippe Lang
33 1 Jean-Philippe Lang
<pre>
34 105 Mizuki ISHIKAWA
$ bundle exec rails generate redmine_plugin Polls
35 67 Jean-Philippe Lang
      create  plugins/polls/app
36 67 Jean-Philippe Lang
      create  plugins/polls/app/controllers
37 67 Jean-Philippe Lang
      create  plugins/polls/app/helpers
38 67 Jean-Philippe Lang
      create  plugins/polls/app/models
39 1 Jean-Philippe Lang
      create  plugins/polls/app/views
40 1 Jean-Philippe Lang
      create  plugins/polls/db/migrate
41 1 Jean-Philippe Lang
      create  plugins/polls/lib/tasks
42 1 Jean-Philippe Lang
      create  plugins/polls/assets/images
43 1 Jean-Philippe Lang
      create  plugins/polls/assets/javascripts
44 67 Jean-Philippe Lang
      create  plugins/polls/assets/stylesheets
45 67 Jean-Philippe Lang
      create  plugins/polls/config/locales
46 67 Jean-Philippe Lang
      create  plugins/polls/test
47 105 Mizuki ISHIKAWA
      create  plugins/polls/test/fixtures
48 105 Mizuki ISHIKAWA
      create  plugins/polls/test/unit
49 105 Mizuki ISHIKAWA
      create  plugins/polls/test/functional
50 105 Mizuki ISHIKAWA
      create  plugins/polls/test/integration
51 105 Mizuki ISHIKAWA
      create  plugins/polls/test/system
52 67 Jean-Philippe Lang
      create  plugins/polls/README.rdoc
53 67 Jean-Philippe Lang
      create  plugins/polls/init.rb
54 1 Jean-Philippe Lang
      create  plugins/polls/config/routes.rb
55 1 Jean-Philippe Lang
      create  plugins/polls/config/locales/en.yml
56 67 Jean-Philippe Lang
      create  plugins/polls/test/test_helper.rb
57 1 Jean-Philippe Lang
</pre>
58 1 Jean-Philippe Lang
59 1 Jean-Philippe Lang
The plugin structure is created in @plugins/polls@. Edit @plugins/polls/init.rb@ to adjust plugin information (name, author, description and version):
60 1 Jean-Philippe Lang
61 67 Jean-Philippe Lang
<pre><code class="ruby">
62 1 Jean-Philippe Lang
Redmine::Plugin.register :polls do
63 1 Jean-Philippe Lang
  name 'Polls plugin'
64 105 Mizuki ISHIKAWA
  author 'Author name'
65 105 Mizuki ISHIKAWA
  description 'This is a plugin for Redmine'
66 1 Jean-Philippe Lang
  version '0.0.1'
67 105 Mizuki ISHIKAWA
  url 'http://example.com/path/to/plugin'
68 105 Mizuki ISHIKAWA
  author_url 'http://example.com/about'
69 1 Jean-Philippe Lang
end
70 1 Jean-Philippe Lang
</code></pre>
71 1 Jean-Philippe Lang
72 1 Jean-Philippe Lang
Then restart the application and point your browser to http://localhost:3000/admin/plugins.
73 1 Jean-Philippe Lang
After logging in, you should see your new plugin in the plugins list:
74 71 Jean-Philippe Lang
75 72 Jean-Philippe Lang
p=. !plugins_list1.png!
76 72 Jean-Philippe Lang
77 1 Jean-Philippe Lang
Note: any change to the @init.rb@ file of your plugin requires to restart the application as it is not reloaded on each request.
78 1 Jean-Philippe Lang
79 1 Jean-Philippe Lang
h2. Generating a model
80 1 Jean-Philippe Lang
81 1 Jean-Philippe Lang
For now plugin doesn't store anything. Let's create a simple Poll model for our plugin. Syntax is:
82 1 Jean-Philippe Lang
83 1 Jean-Philippe Lang
<pre>
84 105 Mizuki ISHIKAWA
   bundle exec rails generate redmine_plugin_model <plugin_name> <model_name> [field[:type][:index] field[:type][:index] ...]
85 1 Jean-Philippe Lang
</pre>
86 1 Jean-Philippe Lang
87 1 Jean-Philippe Lang
So, go to the command prompt and run:
88 1 Jean-Philippe Lang
89 1 Jean-Philippe Lang
<pre>
90 105 Mizuki ISHIKAWA
$ bundle exec rails generate redmine_plugin_model polls poll question:string yes:integer no:integer
91 67 Jean-Philippe Lang
      create  plugins/polls/app/models/poll.rb
92 67 Jean-Philippe Lang
      create  plugins/polls/test/unit/poll_test.rb
93 105 Mizuki ISHIKAWA
      create  plugins/polls/db/migrate/xxxxxxxxxxxx_create_polls.rb
94 1 Jean-Philippe Lang
</pre>
95 67 Jean-Philippe Lang
96 105 Mizuki ISHIKAWA
This creates the Poll model and the corresponding migration file @xxxxxxxxxxxx_create_polls.rb@ in @plugins/polls/db/migrate@:
97 1 Jean-Philippe Lang
98 67 Jean-Philippe Lang
<pre><code class="ruby">
99 105 Mizuki ISHIKAWA
class CreatePolls < ActiveRecord::Migration[5.2]
100 67 Jean-Philippe Lang
  def change
101 67 Jean-Philippe Lang
    create_table :polls do |t|
102 67 Jean-Philippe Lang
      t.string :question
103 105 Mizuki ISHIKAWA
      t.integer :yes, default: 0
104 105 Mizuki ISHIKAWA
      t.integer :no, default: 0
105 67 Jean-Philippe Lang
    end
106 67 Jean-Philippe Lang
  end
107 1 Jean-Philippe Lang
end
108 1 Jean-Philippe Lang
</code></pre>
109 1 Jean-Philippe Lang
110 105 Mizuki ISHIKAWA
NOTE: For Redmine 3.x @class CreatePolls < ActiveRecord::Migration[5.2]@ is @class CreatePolls < ActiveRecord::Migration@.
111 105 Mizuki ISHIKAWA
112 14 Jean-Philippe Lang
You can adjust your migration file (eg. default values...) then migrate the database using the following command:
113 67 Jean-Philippe Lang
114 100 Toshi MARUYAMA
<pre>
115 1 Jean-Philippe Lang
$ bundle exec rake redmine:plugins:migrate
116 67 Jean-Philippe Lang
117 67 Jean-Philippe Lang
Migrating polls (Polls plugin)...
118 67 Jean-Philippe Lang
==  CreatePolls: migrating ====================================================
119 67 Jean-Philippe Lang
-- create_table(:polls)
120 67 Jean-Philippe Lang
   -> 0.0410s
121 67 Jean-Philippe Lang
==  CreatePolls: migrated (0.0420s) ===========================================
122 64 Denny Schäfer
</pre>
123 64 Denny Schäfer
124 104 Go MAEDA
Note that each plugin has its own set of migrations.
125 24 Eric Davis
126 100 Toshi MARUYAMA
Let's add some Polls in the console so we have something to work with.  The console is where you can 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
127 77 mina Beshay
128 1 Jean-Philippe Lang
<pre>
129 105 Mizuki ISHIKAWA
bundle exec rails console
130 105 Mizuki ISHIKAWA
>> Poll.create(question: "Can you see this poll")
131 105 Mizuki ISHIKAWA
>> Poll.create(question: "And can you see this other poll")
132 1 Jean-Philippe Lang
>> exit
133 15 Jean-Philippe Lang
</pre>
134 15 Jean-Philippe Lang
135 67 Jean-Philippe Lang
Edit @plugins/polls/app/models/poll.rb@ in your plugin directory to add a #vote method that will be invoked from our controller:
136 1 Jean-Philippe Lang
137 15 Jean-Philippe Lang
<pre><code class="ruby">
138 15 Jean-Philippe Lang
class Poll < ActiveRecord::Base
139 1 Jean-Philippe Lang
  def vote(answer)
140 1 Jean-Philippe Lang
    increment(answer == 'yes' ? :yes : :no)
141 60 Mischa The Evil
  end
142 57 Etienne Massip
end
143 9 Jean-Philippe Lang
</code></pre>
144 9 Jean-Philippe Lang
145 67 Jean-Philippe Lang
h2. Generating a controller
146 3 Jean-Philippe Lang
147 3 Jean-Philippe Lang
For now, the plugin doesn't do anything. So let's create a controller for our plugin.
148 1 Jean-Philippe Lang
We can use the plugin controller generator for that. Syntax is:
149 100 Toshi MARUYAMA
150 105 Mizuki ISHIKAWA
<pre>bundle exec rails generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]</pre>
151 1 Jean-Philippe Lang
152 1 Jean-Philippe Lang
So go back to the command prompt and run:
153 1 Jean-Philippe Lang
154 101 Toshi MARUYAMA
<pre>
155 105 Mizuki ISHIKAWA
$ bundle exec rails generate redmine_plugin_controller Polls polls index vote
156 67 Jean-Philippe Lang
      create  plugins/polls/app/controllers/polls_controller.rb
157 67 Jean-Philippe Lang
      create  plugins/polls/app/helpers/polls_helper.rb
158 67 Jean-Philippe Lang
      create  plugins/polls/test/functional/polls_controller_test.rb
159 67 Jean-Philippe Lang
      create  plugins/polls/app/views/polls/index.html.erb
160 1 Jean-Philippe Lang
      create  plugins/polls/app/views/polls/vote.html.erb
161 3 Jean-Philippe Lang
</pre>
162 67 Jean-Philippe Lang
163 1 Jean-Philippe Lang
A controller @PollsController@ with 2 actions (@#index@ and @#vote@) is created.
164 1 Jean-Philippe Lang
165 1 Jean-Philippe Lang
Edit @plugins/polls/app/controllers/polls_controller.rb@ to implement these 2 actions.
166 1 Jean-Philippe Lang
167 1 Jean-Philippe Lang
<pre><code class="ruby">
168 3 Jean-Philippe Lang
class PollsController < ApplicationController
169 7 Jean-Philippe Lang
  def index
170 72 Jean-Philippe Lang
    @polls = Poll.all
171 25 Eric Davis
  end
172 25 Eric Davis
173 1 Jean-Philippe Lang
  def vote
174 25 Eric Davis
    poll = Poll.find(params[:id])
175 3 Jean-Philippe Lang
    poll.vote(params[:answer])
176 1 Jean-Philippe Lang
    if poll.save
177 1 Jean-Philippe Lang
      flash[:notice] = 'Vote saved.'
178 3 Jean-Philippe Lang
    end
179 105 Mizuki ISHIKAWA
    redirect_to polls_path(project_id: params[:project_id])
180 5 Jean-Philippe Lang
  end
181 26 Eric Davis
end
182 3 Jean-Philippe Lang
</code></pre>
183 3 Jean-Philippe Lang
184 67 Jean-Philippe Lang
Then edit @plugins/polls/app/views/polls/index.html.erb@ that will display existing polls:
185 3 Jean-Philippe Lang
186 74 Etienne Massip
<pre><code class="erb">
187 3 Jean-Philippe Lang
<h2>Polls</h2>
188 19 Jean-Philippe Lang
189 50 Igor Zubkov
<% @polls.each do |poll| %>
190 1 Jean-Philippe Lang
  <p>
191 105 Mizuki ISHIKAWA
    <%= poll.question %>?
192 105 Mizuki ISHIKAWA
    <%= link_to 'Yes', { action: 'vote', id: poll[:id], answer: 'yes', project_id: @project }, method: :post %> <%= poll.yes %> /
193 105 Mizuki ISHIKAWA
    <%= link_to 'No', { action: 'vote', id: poll[:id], answer: 'no', project_id: @project }, method: :post %> <%= poll.no %>
194 1 Jean-Philippe Lang
  </p>
195 3 Jean-Philippe Lang
<% end %>
196 74 Etienne Massip
</code></pre>
197 72 Jean-Philippe Lang
198 18 Jean-Philippe Lang
You can remove @plugins/polls/app/views/polls/vote.html.erb@ since no rendering is done by the @#vote@ action.
199 72 Jean-Philippe Lang
200 1 Jean-Philippe Lang
h3. Adding routes
201 72 Jean-Philippe Lang
202 72 Jean-Philippe Lang
Redmine does not provide the default wildcard route (@':controller/:action/:id'@). Plugins have to declare the routes they need in their proper @config/routes.rb@ file. So edit @plugins/polls/config/routes.rb@ to add the 2 routes for the 2 actions:
203 1 Jean-Philippe Lang
204 67 Jean-Philippe Lang
<pre><code class="ruby">
205 105 Mizuki ISHIKAWA
get 'polls', to: 'polls#index'
206 105 Mizuki ISHIKAWA
post 'post/:id/vote', to: 'polls#vote'
207 67 Jean-Philippe Lang
</code></pre>
208 67 Jean-Philippe Lang
209 72 Jean-Philippe Lang
You can find more information about Rails routes here: http://guides.rubyonrails.org/routing.html.
210 72 Jean-Philippe Lang
211 38 Randy Syring
Now, restart the application and point your browser to http://localhost:3000/polls.
212 1 Jean-Philippe Lang
You should see the 2 polls and you should be able to vote for them:
213 38 Randy Syring
214 71 Jean-Philippe Lang
p=. !pools1.png!
215 4 Jean-Philippe Lang
216 72 Jean-Philippe Lang
h2. Internationalization
217 4 Jean-Philippe Lang
218 67 Jean-Philippe Lang
The translation files must be stored in config/locales, eg. @plugins/polls/config/locales/@.
219 1 Jean-Philippe Lang
220 1 Jean-Philippe Lang
h2. Extending menus
221 4 Jean-Philippe Lang
222 4 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.
223 18 Jean-Philippe Lang
So let's add a new item to the application menu.
224 4 Jean-Philippe Lang
225 18 Jean-Philippe Lang
h3. Extending the application menu
226 4 Jean-Philippe Lang
227 1 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ at the root of your plugin directory to add the following line at the end of the plugin registration block:
228 1 Jean-Philippe Lang
229 4 Jean-Philippe Lang
<pre><code class="ruby">
230 4 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
231 1 Jean-Philippe Lang
  [...]
232 4 Jean-Philippe Lang
  
233 105 Mizuki ISHIKAWA
  menu :application_menu, :polls, { controller: 'polls', action: 'index' }, caption: 'Polls'
234 1 Jean-Philippe Lang
end
235 4 Jean-Philippe Lang
</code></pre>
236 42 Mischa The Evil
237 4 Jean-Philippe Lang
Syntax is:
238 4 Jean-Philippe Lang
239 4 Jean-Philippe Lang
  menu(menu_name, item_name, url, options={})
240 1 Jean-Philippe Lang
241 4 Jean-Philippe Lang
There are five menus that you can extend:
242 4 Jean-Philippe Lang
243 1 Jean-Philippe Lang
* @:top_menu@ - the top left menu
244 1 Jean-Philippe Lang
* @:account_menu@ - the top right menu with sign in/sign out links
245 4 Jean-Philippe Lang
* @:application_menu@ - the main menu displayed when the user is not inside a project
246 4 Jean-Philippe Lang
* @:project_menu@ - the main menu displayed when the user is inside a project
247 4 Jean-Philippe Lang
* @:admin_menu@ - the menu displayed on the Administration page (can only insert after Settings, before Plugins)
248 4 Jean-Philippe Lang
249 4 Jean-Philippe Lang
Available options are:
250 4 Jean-Philippe Lang
251 1 Jean-Philippe Lang
* @:param@ - the parameter key that is used for the project id (default is @:id@)
252 36 Jérémie Delaitre
* @:if@ - a Proc that is called before rendering the item, the item is displayed only if it returns true
253 4 Jean-Philippe Lang
* @:caption@ - the menu caption that can be:
254 4 Jean-Philippe Lang
255 4 Jean-Philippe Lang
  * a localized string Symbol
256 29 Vinod Singh
  * a String
257 4 Jean-Philippe Lang
  * a Proc that can take the project as argument
258 1 Jean-Philippe Lang
259 105 Mizuki ISHIKAWA
* @:before@, @:after@ - specify where the menu item should be inserted (eg. @after: :activity@)
260 105 Mizuki ISHIKAWA
* @:first@, @:last@ - if set to true, the item will stay at the beginning/end of the menu (eg. @last: true@)
261 4 Jean-Philippe Lang
* @:html@ - a hash of html options that are passed to @link_to@ when rendering the menu item
262 1 Jean-Philippe Lang
263 104 Go MAEDA
In our example, we've added an item to the application menu which is empty by default.
264 105 Mizuki ISHIKAWA
Restart the application and go to http://localhost:3000/projects:
265 6 Jean-Philippe Lang
266 71 Jean-Philippe Lang
p=. !application_menu.png!
267 6 Jean-Philippe Lang
268 105 Mizuki ISHIKAWA
Now you can access the polls by clicking the Polls tab that appears when  the user is not inside a project.
269 6 Jean-Philippe Lang
270 6 Jean-Philippe Lang
h3. Extending the project menu
271 51 Igor Zubkov
272 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 poll model). So we would like to add the Polls tab to the project menu instead.
273 6 Jean-Philippe Lang
Open @init.rb@ and replace the line that was added just before with these 2 lines:
274 6 Jean-Philippe Lang
275 6 Jean-Philippe Lang
<pre><code class="ruby">
276 18 Jean-Philippe Lang
Redmine::Plugin.register :redmine_polls do
277 18 Jean-Philippe Lang
  [...]
278 6 Jean-Philippe Lang
279 105 Mizuki ISHIKAWA
  permission :polls, { polls: [:index, :vote] }, public: true
280 105 Mizuki ISHIKAWA
  menu :project_menu, :polls, { controller: 'polls', action: 'index' }, caption: 'Polls', after: :activity, param: :project_id
281 1 Jean-Philippe Lang
end
282 6 Jean-Philippe Lang
</code></pre>
283 67 Jean-Philippe Lang
284 6 Jean-Philippe Lang
The second line adds our Polls tab to the project menu, just after the activity tab. 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. Restart the application again and go to one of your projects:
285 67 Jean-Philippe Lang
286 1 Jean-Philippe Lang
p=. !project_menu.png!
287 19 Jean-Philippe Lang
288 6 Jean-Philippe Lang
If you click the Polls tab (in 3rd position), you should notice that the project menu is no longer displayed.
289 1 Jean-Philippe Lang
To make the project menu visible, you have to initialize the controller's instance variable @@project@.
290 1 Jean-Philippe Lang
291 61 Harry Garrood
Edit your PollsController to do so:
292 61 Harry Garrood
293 6 Jean-Philippe Lang
<pre><code class="ruby">
294 6 Jean-Philippe Lang
def index
295 18 Jean-Philippe Lang
  @project = Project.find(params[:project_id])
296 105 Mizuki ISHIKAWA
  @polls = Poll.all # @project.polls
297 39 Ric Turley
end
298 1 Jean-Philippe Lang
</code></pre>
299 4 Jean-Philippe Lang
300 105 Mizuki ISHIKAWA
The project id is available in the @:project_id@ param because of the @param: :project_id@ option in the menu item declaration above.
301 18 Jean-Philippe Lang
302 71 Jean-Philippe Lang
Now, you should see the project menu when viewing the polls:
303 10 Jean-Philippe Lang
304 1 Jean-Philippe Lang
p=. !project_menu_pools.png!
305 103 luigifab !
306 103 luigifab !
h3. Removing item in menu
307 103 luigifab !
308 1 Jean-Philippe Lang
To remove an item in a menu, you can use @delete_menu_item@ like in this example:
309 103 luigifab !
310 103 luigifab !
<pre><code class="ruby">
311 103 luigifab !
Redmine::Plugin.register :redmine_polls do
312 103 luigifab !
  [...]
313 103 luigifab !
314 103 luigifab !
  delete_menu_item :top_menu, :my_page
315 103 luigifab !
  delete_menu_item :top_menu, :help
316 103 luigifab !
  delete_menu_item :project_menu, :overview
317 103 luigifab !
  delete_menu_item :project_menu, :activity
318 103 luigifab !
  delete_menu_item :project_menu, :news
319 103 luigifab !
end
320 103 luigifab !
</code></pre>
321 103 luigifab !
322 10 Jean-Philippe Lang
h2. Adding new permissions
323 20 Jean-Philippe Lang
324 18 Jean-Philippe Lang
For now, anyone can vote for polls. Let's make it more configurable by changing the permission declaration.
325 105 Mizuki ISHIKAWA
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).
326 1 Jean-Philippe Lang
327 67 Jean-Philippe Lang
Edit @plugins/polls/init.rb@ to replace the previous permission declaration with these 2 lines:
328 10 Jean-Philippe Lang
329 10 Jean-Philippe Lang
<pre><code class="ruby">
330 105 Mizuki ISHIKAWA
  permission :view_polls, polls: :index
331 105 Mizuki ISHIKAWA
  permission :vote_polls, polls: :vote
332 1 Jean-Philippe Lang
</code></pre>
333 10 Jean-Philippe Lang
334 89 Robert Schneider
Restart the application and go to http://localhost:3000/roles/permissions:
335 18 Jean-Philippe Lang
336 71 Jean-Philippe Lang
p=. !permissions1.png!
337 10 Jean-Philippe Lang
338 10 Jean-Philippe Lang
You're now able to give these permissions to your existing roles.
339 1 Jean-Philippe Lang
340 67 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. 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.
341 10 Jean-Philippe Lang
342 10 Jean-Philippe Lang
Here is how it would look like for the @#index@ action:
343 10 Jean-Philippe Lang
344 10 Jean-Philippe Lang
<pre><code class="ruby">
345 19 Jean-Philippe Lang
class PollsController < ApplicationController
346 109 keineahnung 2345
  before_action :find_project, :authorize, only: [:index, :vote]
347 1 Jean-Philippe Lang
348 10 Jean-Philippe Lang
  [...]
349 10 Jean-Philippe Lang
  
350 10 Jean-Philippe Lang
  def index
351 105 Mizuki ISHIKAWA
    @polls = Poll.all # @project.polls
352 10 Jean-Philippe Lang
  end
353 10 Jean-Philippe Lang
354 10 Jean-Philippe Lang
  [...]
355 10 Jean-Philippe Lang
  
356 10 Jean-Philippe Lang
  private
357 10 Jean-Philippe Lang
  
358 10 Jean-Philippe Lang
  def find_project
359 18 Jean-Philippe Lang
    # @project variable must be set before calling the authorize filter
360 1 Jean-Philippe Lang
    @project = Project.find(params[:project_id])
361 1 Jean-Philippe Lang
  end
362 1 Jean-Philippe Lang
end
363 4 Jean-Philippe Lang
</code></pre>
364 1 Jean-Philippe Lang
365 31 Markus Bockman
Retrieving the current project before the @#vote@ action could be done using a similar way.
366 37 Randy Syring
After this, viewing and voting polls will be only available to admin users or users that have the appropriate role on the project.
367 31 Markus Bockman
368 104 Go MAEDA
If you want to display the symbols of your permissions in a multilingual way, you need to add the necessary text labels in a language file.
369 83 Denis Savitskiy
Simply create an *.yml (eg. @en.yml@) file in @plugins/polls/config/locales@ and fill it with labels like this:
370 86 Lennart Nordgreen
371 31 Markus Bockman
<pre><code class="yaml">
372 31 Markus Bockman
"en":
373 83 Denis Savitskiy
  permission_view_polls: View Polls
374 1 Jean-Philippe Lang
  permission_vote_polls: Vote Polls
375 1 Jean-Philippe Lang
</code></pre>
376 67 Jean-Philippe Lang
377 31 Markus Bockman
In this example the created file is known as @en.yml@, but all other supported language files are also possible too.
378 4 Jean-Philippe Lang
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. 
379 4 Jean-Philippe Lang
380 56 Thomas Winkel
Restart your application and point the permission section.
381 1 Jean-Philippe Lang
382 26 Eric Davis
h2. Creating a project module
383 11 Jean-Philippe Lang
384 1 Jean-Philippe Lang
For now, the poll functionality is added to all your projects. But you may want to enable polls for some projects only.
385 11 Jean-Philippe Lang
So, let's create a 'Polls' project module. This is done by wrapping the permissions declaration inside a call to @#project_module@.
386 11 Jean-Philippe Lang
387 11 Jean-Philippe Lang
Edit @init.rb@ and change the permissions declaration:
388 18 Jean-Philippe Lang
389 18 Jean-Philippe Lang
<pre><code class="ruby">
390 18 Jean-Philippe Lang
  project_module :polls do
391 105 Mizuki ISHIKAWA
    permission :view_polls, polls: :index
392 105 Mizuki ISHIKAWA
    permission :vote_polls, polls: :vote
393 11 Jean-Philippe Lang
  end
394 11 Jean-Philippe Lang
</code></pre>
395 18 Jean-Philippe Lang
396 11 Jean-Philippe Lang
Restart the application and go to one of your project settings.
397 29 Vinod Singh
Click on the Modules tab. You should see the Polls module at the end of the modules list (disabled by default):
398 11 Jean-Philippe Lang
399 71 Jean-Philippe Lang
p=. !modules.png!
400 11 Jean-Philippe Lang
401 11 Jean-Philippe Lang
You can now enable/disable polls at project level.
402 11 Jean-Philippe Lang
403 16 Jean-Philippe Lang
h2. Improving the plugin views
404 16 Jean-Philippe Lang
405 16 Jean-Philippe Lang
h3. Adding stylesheets
406 26 Eric Davis
407 16 Jean-Philippe Lang
Let's start by adding a stylesheet to our plugin views.
408 67 Jean-Philippe Lang
Create a file named @voting.css@ in the @plugins/polls/assets/stylesheets@ directory:
409 16 Jean-Philippe Lang
410 83 Denis Savitskiy
<pre><code class="css">
411 16 Jean-Philippe Lang
a.vote { font-size: 120%; }
412 16 Jean-Philippe Lang
a.vote.yes { color: green; }
413 16 Jean-Philippe Lang
a.vote.no  { color: red; }
414 83 Denis Savitskiy
</code></pre>
415 16 Jean-Philippe Lang
416 67 Jean-Philippe Lang
When starting the application, plugin assets are automatically copied to @public/plugin_assets/polls/@ to make them available through your web server. So any change to your plugin stylesheets or javascripts needs an application restart.
417 16 Jean-Philippe Lang
418 90 Robert Schneider
The introduced classes need to be used by the links. So change in file @plugins/polls/app/views/polls/index.html.erb@ the link declarations to:
419 90 Robert Schneider
420 90 Robert Schneider
<pre><code class="erb">
421 110 keineahnung 2345
<%= link_to 'Yes', {action: 'vote', id: poll[:id], answer: 'yes', project_id: @project }, method: :post, class: 'vote yes' %> (<%= poll.yes %>)
422 110 keineahnung 2345
<%= link_to 'No', {action: 'vote', id: poll[:id], answer: 'no', project_id: @project }, method: :post, class: 'vote no' %> (<%= poll.no %>)
423 90 Robert Schneider
</code></pre>
424 90 Robert Schneider
425 16 Jean-Philippe Lang
Then, append the following lines at the end of @index.html.erb@ so that your stylesheet get included in the page header by Redmine:
426 84 Mischa The Evil
427 16 Jean-Philippe Lang
<pre><code class="erb">
428 73 Jean-Philippe Lang
<% content_for :header_tags do %>
429 105 Mizuki ISHIKAWA
    <%= stylesheet_link_tag 'voting', plugin: 'polls' %>
430 1 Jean-Philippe Lang
<% end %>
431 83 Denis Savitskiy
</code></pre>
432 16 Jean-Philippe Lang
433 105 Mizuki ISHIKAWA
Note that the @plugin: 'polls'@ option is required when calling the @stylesheet_link_tag@ helper.
434 16 Jean-Philippe Lang
435 16 Jean-Philippe Lang
Javascripts can be included in plugin views using the @javascript_include_tag@ helper in the same way.
436 1 Jean-Philippe Lang
437 16 Jean-Philippe Lang
h3. Setting page title
438 1 Jean-Philippe Lang
439 16 Jean-Philippe Lang
You can set the HTML title from inside your views by using the @html_title@ helper.
440 1 Jean-Philippe Lang
Example:
441 84 Mischa The Evil
442 34 Tom Bostelmann
<pre><code class="erb">
443 83 Denis Savitskiy
  <% html_title "Polls" %>
444 34 Tom Bostelmann
</code></pre>
445 75 Jean-Philippe Lang
446 75 Jean-Philippe Lang
h2. Using hooks
447 1 Jean-Philippe Lang
448 76 Jean-Philippe Lang
h3. Hooks in views
449 76 Jean-Philippe Lang
450 76 Jean-Philippe Lang
Hooks in Redmine views lets you insert custom content to regular Redmine views. For example, looking at source:tags/2.0.0/app/views/projects/show.html.erb#L52 shows that there are 2 hooks available: one named @:view_projects_show_left@ for adding content to the left part and one named @:view_projects_show_right@ for adding content to the right part of the view.
451 76 Jean-Philippe Lang
452 91 Robert Schneider
To use one or more hooks in views, you need to create a class that inherits from @Redmine::Hook::ViewListener@ and implement methods named with the hook(s) you want to use. To append some content to the project overview, add a class to your plugin and require it in your @init.rb@, then implement methods whose name match the hook names.
453 76 Jean-Philippe Lang
454 92 Robert Schneider
For our plugin create a file @plugins/polls/lib/polls_hook_listener.rb@ with this content:
455 76 Jean-Philippe Lang
456 76 Jean-Philippe Lang
<pre><code class="ruby">
457 1 Jean-Philippe Lang
class PollsHookListener < Redmine::Hook::ViewListener
458 91 Robert Schneider
  def view_projects_show_left(context = {})
459 76 Jean-Philippe Lang
    return content_tag("p", "Custom content added to the left")
460 1 Jean-Philippe Lang
  end
461 76 Jean-Philippe Lang
462 91 Robert Schneider
  def view_projects_show_right(context = {})
463 1 Jean-Philippe Lang
    return content_tag("p", "Custom content added to the right")
464 1 Jean-Philippe Lang
  end
465 1 Jean-Philippe Lang
end
466 1 Jean-Philippe Lang
</code></pre>
467 1 Jean-Philippe Lang
468 91 Robert Schneider
Prepend this line to @plugins/polls/init.rb@:
469 91 Robert Schneider
470 91 Robert Schneider
<pre><code class="ruby">
471 92 Robert Schneider
require_dependency 'polls_hook_listener'
472 91 Robert Schneider
</code></pre>
473 91 Robert Schneider
474 92 Robert Schneider
Restart Redmine and have a look into the overview tab of a project. You should see the strings on the left and the right side in the overview.
475 76 Jean-Philippe Lang
476 92 Robert Schneider
You can also use the @render_on@ helper to render a partial. In our plugin you have to replace the just created content in @plugins/polls/lib/polls_hook_listener.rb@ with:
477 76 Jean-Philippe Lang
478 1 Jean-Philippe Lang
<pre><code class="ruby">
479 76 Jean-Philippe Lang
class PollsHookListener < Redmine::Hook::ViewListener
480 105 Mizuki ISHIKAWA
  render_on :view_projects_show_left, partial: "polls/project_overview"
481 76 Jean-Philippe Lang
end
482 76 Jean-Philippe Lang
</code></pre>
483 76 Jean-Philippe Lang
484 93 Robert Schneider
Add the partial to your plugin by creating the file @app/views/polls/_project_overview.html.erb@. Its content (use some text like 'Message from Hook!') will be appended to the left part of the project overview. Don't forget to restart Redmine.
485 76 Jean-Philippe Lang
486 76 Jean-Philippe Lang
h3. Hooks in controllers
487 76 Jean-Philippe Lang
488 75 Jean-Philippe Lang
TODO
489 75 Jean-Philippe Lang
490 75 Jean-Philippe Lang
h2. Making your plugin configurable
491 75 Jean-Philippe Lang
492 81 Paul Kerr
Each plugin registered with Redmine is displayed on the admin/plugins page. Support for a basic configuration mechanism is supplied by the Settings controller. This feature is enabled by adding the "settings" method to the plugin registration block in a plugin's init.rb file.
493 81 Paul Kerr
494 81 Paul Kerr
<pre><code class="ruby">
495 106 Mischa The Evil
Redmine::Plugin.register :polls do
496 81 Paul Kerr
  [ ... ]
497 81 Paul Kerr
498 105 Mizuki ISHIKAWA
  settings default: {'empty' => true}, partial: 'settings/poll_settings'
499 81 Paul Kerr
end
500 81 Paul Kerr
</code></pre>
501 81 Paul Kerr
502 106 Mischa The Evil
Adding this will accomplish two things. First, it will add a "Configure" link to the description block for the plugin in the admin/plugins list. Following this link will cause a common plugin configuration template view to be loaded which will in turn render the partial view referenced by :partial. Calling the settings method will also add support in the Setting module for the plugin. The Setting model will store and retrieve a serialized hash based on the plugin name. This hash is accessed using the Setting method name in the form plugin_<plugin name>. For this example, the hash can be accessed by calling Setting.plugin_polls.
503 81 Paul Kerr
504 81 Paul Kerr
p=. !plugin_with_config.png!
505 81 Paul Kerr
506 81 Paul Kerr
The view referenced by the :partial hash key passed to the settings method will be loaded as a partial within the plugin configuration view. The basic page layout is constrained by the plugin configuration view: a form is declared and the submit button is generated. The partial is pulled into the view inside a table div inside the form. Configuration settings for the plugin will be displayed and can be modified via standard HTML form elements.
507 81 Paul Kerr
508 81 Paul Kerr
p=. !plugin_config_view.png!
509 81 Paul Kerr
510 85 Jean-Baptiste Barth
*NB* : if two plugins have the same partial name for settings, the first will override the second's settings page. So be sure you give a unique name to your settings partial.
511 85 Jean-Baptiste Barth
512 106 Mischa The Evil
When the page is submitted, the settings_controller will take the parameter hash referenced by 'settings' and store it directly in a serialized format in Setting.plugin_polls. Each time the page is generated the current value of Setting.plugin_polls will be assigned to the local variable settings.
513 81 Paul Kerr
514 81 Paul Kerr
<pre><code class="erb">
515 81 Paul Kerr
<table>
516 81 Paul Kerr
  <tbody>
517 81 Paul Kerr
    <tr>
518 98 Denis Savitskiy
      <th>Notification Default Address</th>
519 98 Denis Savitskiy
      <td>
520 1 Jean-Philippe Lang
        <input type="text" id="settings_notification_default"
521 98 Denis Savitskiy
                           value="<%= settings['notification_default'] %>"
522 98 Denis Savitskiy
	                       name="settings[notification_default]" >
523 98 Denis Savitskiy
      </td>
524 81 Paul Kerr
    </tr>
525 81 Paul Kerr
  </tbody>
526 81 Paul Kerr
</table>
527 81 Paul Kerr
</code></pre>
528 81 Paul Kerr
529 106 Mischa The Evil
In the example above, the configuration form was not created using Rails form helpers. This is because there is no @settings model but only the setting hash. Form helpers will attempt to access attributes using model accessor methods which do not exist. For example, a call to @settings.notification_default will fail. The value set by this form is accessed as Setting.plugin_polls['notification_default'].
530 81 Paul Kerr
531 106 Mischa The Evil
Finally, the :default in the settings method call is to register a value that will be returned from the Setting.plugin_polls call if nothing has been stored in the settings table for this plugin.
532 75 Jean-Philippe Lang
533 34 Tom Bostelmann
h2. Testing your plugin
534 34 Tom Bostelmann
535 71 Jean-Philippe Lang
h3. test/test_helper.rb
536 34 Tom Bostelmann
537 34 Tom Bostelmann
Here are the contents of my test helper file:
538 34 Tom Bostelmann
539 1 Jean-Philippe Lang
<pre>
540 69 Jean-Philippe Lang
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
541 34 Tom Bostelmann
</pre>
542 34 Tom Bostelmann
543 70 Jean-Philippe Lang
h3. Sample test
544 34 Tom Bostelmann
545 108 Juno NISHIZAKI
Contents of @polls_controller_test.rb@ for Redmine 4.x:
546 108 Juno NISHIZAKI
547 108 Juno NISHIZAKI
<pre><code class="ruby">
548 108 Juno NISHIZAKI
require File.expand_path('../../test_helper', __FILE__)
549 108 Juno NISHIZAKI
550 108 Juno NISHIZAKI
class PollsControllerTest < ActionController::TestCase
551 108 Juno NISHIZAKI
  fixtures :projects
552 108 Juno NISHIZAKI
553 108 Juno NISHIZAKI
  def test_index
554 108 Juno NISHIZAKI
    get :index, params: { project_id: 1 }
555 108 Juno NISHIZAKI
556 108 Juno NISHIZAKI
    assert_response :success
557 108 Juno NISHIZAKI
    assert_template 'index'
558 108 Juno NISHIZAKI
  end
559 108 Juno NISHIZAKI
end
560 108 Juno NISHIZAKI
</code></pre>
561 108 Juno NISHIZAKI
562 108 Juno NISHIZAKI
Contents of @polls_controller_test.rb@ for Redmine 3.x:
563 1 Jean-Philippe Lang
564 1 Jean-Philippe Lang
<pre><code class="ruby">
565 69 Jean-Philippe Lang
require File.expand_path('../../test_helper', __FILE__)
566 34 Tom Bostelmann
567 69 Jean-Philippe Lang
class PollsControllerTest < ActionController::TestCase
568 69 Jean-Philippe Lang
  fixtures :projects
569 34 Tom Bostelmann
570 69 Jean-Philippe Lang
  def test_index
571 105 Mizuki ISHIKAWA
    get :index, project_id: 1
572 34 Tom Bostelmann
573 88 Niklaus Giger
    assert_response :success
574 69 Jean-Philippe Lang
    assert_template 'index'
575 34 Tom Bostelmann
  end
576 52 Igor Zubkov
end
577 54 Igor Zubkov
</code></pre>
578 34 Tom Bostelmann
579 70 Jean-Philippe Lang
h3. Running test
580 34 Tom Bostelmann
581 70 Jean-Philippe Lang
Initialize the test database if necessary:
582 68 Jean-Philippe Lang
583 34 Tom Bostelmann
<pre>
584 87 Vincent Robert
$ rake db:drop db:create db:migrate redmine:plugins:migrate redmine:load_default_data RAILS_ENV=test
585 48 Igor Zubkov
</pre>
586 34 Tom Bostelmann
587 69 Jean-Philippe Lang
To execute the polls_controller_test.rb:
588 34 Tom Bostelmann
589 34 Tom Bostelmann
<pre>
590 107 Mizuki ISHIKAWA
$ RAILS_ENV=test bundle exec rake test TEST=plugins/polls/test/functional/polls_controller_test.rb
591 47 Mo Morsi
</pre>
592 35 Tom Bostelmann
593 35 Tom Bostelmann
h3. Testing with permissions
594 35 Tom Bostelmann
595 1 Jean-Philippe Lang
If your plugin requires membership to a project, add the following to the beginning of your functional tests:
596 47 Mo Morsi
597 82 Denis Savitskiy
<pre><code class="ruby">
598 47 Mo Morsi
def test_index
599 1 Jean-Philippe Lang
  @request.session[:user_id] = 2
600 47 Mo Morsi
  ...
601 1 Jean-Philippe Lang
end
602 82 Denis Savitskiy
</code></pre>
603 47 Mo Morsi
604 47 Mo Morsi
If your plugin requires a specific permission, you can add that to a user role like so (lookup which role is appropriate for the user in the fixtures):
605 1 Jean-Philippe Lang
606 82 Denis Savitskiy
<pre><code class="ruby">
607 47 Mo Morsi
def test_index
608 1 Jean-Philippe Lang
  Role.find(1).add_permission! :my_permission
609 1 Jean-Philippe Lang
  ...
610 35 Tom Bostelmann
end
611 82 Denis Savitskiy
</code></pre>
612 47 Mo Morsi
613 47 Mo Morsi
614 47 Mo Morsi
You may enable/disable a specific module like so:
615 47 Mo Morsi
616 82 Denis Savitskiy
<pre><code class="ruby">
617 47 Mo Morsi
def test_index
618 47 Mo Morsi
  Project.find(1).enabled_module_names = [:mymodule]
619 47 Mo Morsi
  ...
620 1 Jean-Philippe Lang
end
621 82 Denis Savitskiy
</code></pre>
622 94 @ go2null
623 95 @ go2null
h3. Reference file hierarchy
624 94 @ go2null
625 96 @ go2null
Here is a simple list of all the files and directories mentioned in this Tutorial and [[Hooks]].  This is useful to ensure standard paths are used and also useful for newbies to know here files should go.
626 94 @ go2null
627 94 @ go2null
<pre>
628 94 @ go2null
plugins/PLUGIN/README.rdoc
629 94 @ go2null
plugins/PLUGIN/init.rb
630 94 @ go2null
plugins/PLUGIN/app/
631 94 @ go2null
plugins/PLUGIN/app/controllers/
632 95 @ go2null
plugins/PLUGIN/app/controllers/CONTROLLER_controller.rb
633 94 @ go2null
plugins/PLUGIN/app/helpers/
634 96 @ go2null
plugins/PLUGIN/app/helpers/CONTROLLER_helper.rb
635 94 @ go2null
plugins/PLUGIN/app/models/
636 94 @ go2null
plugins/PLUGIN/app/models/MODEL.rb
637 94 @ go2null
plugins/PLUGIN/app/views/
638 95 @ go2null
plugins/PLUGIN/app/views/CONTROLLER/
639 1 Jean-Philippe Lang
plugins/PLUGIN/app/views/CONTROLLER/_PARTIAL.html.erb
640 1 Jean-Philippe Lang
plugins/PLUGIN/app/views/CONTROLLER/CONTROLLER-ACTION.html.erb
641 96 @ go2null
plugins/PLUGIN/app/views/hooks/
642 96 @ go2null
plugins/PLUGIN/app/views/hooks/_HOOK.html.erb
643 94 @ go2null
plugins/PLUGIN/app/views/settings/
644 94 @ go2null
plugins/PLUGIN/app/views/settings/_MODEL_settings.html.erb
645 94 @ go2null
plugins/PLUGIN/assets/
646 94 @ go2null
plugins/PLUGIN/assets/images/
647 94 @ go2null
plugins/PLUGIN/assets/javascripts/
648 94 @ go2null
plugins/PLUGIN/assets/stylesheets/
649 94 @ go2null
plugins/PLUGIN/assets/stylesheets/voting.css
650 94 @ go2null
plugins/PLUGIN/config/
651 94 @ go2null
plugins/PLUGIN/config/locales/
652 94 @ go2null
plugins/PLUGIN/config/locales/en.yml
653 1 Jean-Philippe Lang
plugins/PLUGIN/config/routes.rb
654 94 @ go2null
plugins/PLUGIN/db/
655 94 @ go2null
plugins/PLUGIN/db/migrate/
656 96 @ go2null
plugins/PLUGIN/db/migrate/001_create_MODELs.rb
657 1 Jean-Philippe Lang
plugins/PLUGIN/lib/
658 94 @ go2null
plugins/PLUGIN/lib/PLUGIN_hook_listener.rb
659 96 @ go2null
plugins/PLUGIN/lib/PLUGIN/
660 96 @ go2null
plugins/PLUGIN/lib/PLUGIN/hooks.rb
661 97 @ go2null
plugins/PLUGIN/lib/PLUGIN/MODEL_patch.rb
662 1 Jean-Philippe Lang
plugins/PLUGIN/lib/tasks/
663 94 @ go2null
plugins/PLUGIN/test/
664 94 @ go2null
plugins/PLUGIN/test/test_helper.rb
665 94 @ go2null
plugins/PLUGIN/test/functional/
666 96 @ go2null
plugins/PLUGIN/test/functional/CONTROLLER_controller_test.rb
667 94 @ go2null
plugins/PLUGIN/test/unit/
668 94 @ go2null
plugins/PLUGIN/test/unit/MODEL_test.rb
669 94 @ go2null
</pre>