Revision 1297:0a574315af3e .svn/pristine/32

View differences:

.svn/pristine/32/325cbd5f2592da1a1293c4a50be14ad1338348eb.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require File.expand_path('../../../test_helper', __FILE__)
19

  
20
class ApiTest::GroupsTest < ActionController::IntegrationTest
21
  fixtures :users, :groups_users
22

  
23
  def setup
24
    Setting.rest_api_enabled = '1'
25
  end
26

  
27
  context "GET /groups" do
28
    context ".xml" do
29
      should "require authentication" do
30
        get '/groups.xml'
31
        assert_response 401
32
      end
33

  
34
      should "return groups" do
35
        get '/groups.xml', {}, credentials('admin')
36
        assert_response :success
37
        assert_equal 'application/xml', response.content_type
38

  
39
        assert_select 'groups' do
40
          assert_select 'group' do
41
            assert_select 'name', :text => 'A Team'
42
            assert_select 'id', :text => '10'
43
          end
44
        end
45
      end
46
    end
47

  
48
    context ".json" do
49
      should "require authentication" do
50
        get '/groups.json'
51
        assert_response 401
52
      end
53

  
54
      should "return groups" do
55
        get '/groups.json', {}, credentials('admin')
56
        assert_response :success
57
        assert_equal 'application/json', response.content_type
58

  
59
        json = MultiJson.load(response.body)
60
        groups = json['groups']
61
        assert_kind_of Array, groups
62
        group = groups.detect {|g| g['name'] == 'A Team'}
63
        assert_not_nil group
64
        assert_equal({'id' => 10, 'name' => 'A Team'}, group)
65
      end
66
    end
67
  end
68

  
69
  context "GET /groups/:id" do
70
    context ".xml" do
71
      should "return the group with its users" do
72
        get '/groups/10.xml', {}, credentials('admin')
73
        assert_response :success
74
        assert_equal 'application/xml', response.content_type
75

  
76
        assert_select 'group' do
77
          assert_select 'name', :text => 'A Team'
78
          assert_select 'id', :text => '10'
79
        end
80
      end
81

  
82
      should "include users if requested" do
83
        get '/groups/10.xml?include=users', {}, credentials('admin')
84
        assert_response :success
85
        assert_equal 'application/xml', response.content_type
86

  
87
        assert_select 'group' do
88
          assert_select 'users' do
89
            assert_select 'user', Group.find(10).users.count
90
            assert_select 'user[id=8]'
91
          end
92
        end
93
      end
94

  
95
      should "include memberships if requested" do
96
        get '/groups/10.xml?include=memberships', {}, credentials('admin')
97
        assert_response :success
98
        assert_equal 'application/xml', response.content_type
99

  
100
        assert_select 'group' do
101
          assert_select 'memberships'
102
        end
103
      end
104
    end
105
  end
106

  
107
  context "POST /groups" do
108
    context "with valid parameters" do
109
      context ".xml" do
110
        should "create groups" do
111
          assert_difference('Group.count') do
112
            post '/groups.xml', {:group => {:name => 'Test', :user_ids => [2, 3]}}, credentials('admin')
113
            assert_response :created
114
            assert_equal 'application/xml', response.content_type
115
          end
116
  
117
          group = Group.order('id DESC').first
118
          assert_equal 'Test', group.name
119
          assert_equal [2, 3], group.users.map(&:id).sort
120

  
121
          assert_select 'group' do
122
            assert_select 'name', :text => 'Test'
123
          end
124
        end
125
      end
126
    end
127

  
128
    context "with invalid parameters" do
129
      context ".xml" do
130
        should "return errors" do
131
          assert_no_difference('Group.count') do
132
            post '/groups.xml', {:group => {:name => ''}}, credentials('admin')
133
          end
134
          assert_response :unprocessable_entity
135
          assert_equal 'application/xml', response.content_type
136

  
137
          assert_select 'errors' do
138
            assert_select 'error', :text => /Name can't be blank/
139
          end
140
        end
141
      end
142
    end
143
  end
144

  
145
  context "PUT /groups/:id" do
146
    context "with valid parameters" do
147
      context ".xml" do
148
        should "update the group" do
149
          put '/groups/10.xml', {:group => {:name => 'New name', :user_ids => [2, 3]}}, credentials('admin')
150
          assert_response :ok
151
          assert_equal '', @response.body
152
  
153
          group = Group.find(10)
154
          assert_equal 'New name', group.name
155
          assert_equal [2, 3], group.users.map(&:id).sort
156
        end
157
      end
158
    end
159

  
160
    context "with invalid parameters" do
161
      context ".xml" do
162
        should "return errors" do
163
          put '/groups/10.xml', {:group => {:name => ''}}, credentials('admin')
164
          assert_response :unprocessable_entity
165
          assert_equal 'application/xml', response.content_type
166

  
167
          assert_select 'errors' do
168
            assert_select 'error', :text => /Name can't be blank/
169
          end
170
        end
171
      end
172
    end
173
  end
174

  
175
  context "DELETE /groups/:id" do
176
    context ".xml" do
177
      should "delete the group" do
178
        assert_difference 'Group.count', -1 do
179
          delete '/groups/10.xml', {}, credentials('admin')
180
          assert_response :ok
181
          assert_equal '', @response.body
182
        end
183
      end
184
    end
185
  end
186

  
187
  context "POST /groups/:id/users" do
188
    context ".xml" do
189
      should "add user to the group" do
190
        assert_difference 'Group.find(10).users.count' do
191
          post '/groups/10/users.xml', {:user_id => 5}, credentials('admin')
192
          assert_response :ok
193
          assert_equal '', @response.body
194
        end
195
        assert_include User.find(5), Group.find(10).users
196
      end
197
    end
198
  end
199

  
200
  context "DELETE /groups/:id/users/:user_id" do
201
    context ".xml" do
202
      should "remove user from the group" do
203
        assert_difference 'Group.find(10).users.count', -1 do
204
          delete '/groups/10/users/8.xml', {}, credentials('admin')
205
          assert_response :ok
206
          assert_equal '', @response.body
207
        end
208
        assert_not_include User.find(8), Group.find(10).users
209
      end
210
    end
211
  end
212
end
.svn/pristine/32/3263175b802b3311f08d06f669648333f55d611c.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
require File.expand_path('../../../test_helper', __FILE__)
19

  
20
class RoutingWatchersTest < ActionController::IntegrationTest
21
  def test_watchers
22
    assert_routing(
23
        { :method => 'get', :path => "/watchers/new" },
24
        { :controller => 'watchers', :action => 'new' }
25
      )
26
    assert_routing(
27
        { :method => 'post', :path => "/watchers/append" },
28
        { :controller => 'watchers', :action => 'append' }
29
      )
30
    assert_routing(
31
        { :method => 'post', :path => "/watchers" },
32
        { :controller => 'watchers', :action => 'create' }
33
      )
34
    assert_routing(
35
        { :method => 'post', :path => "/watchers/destroy" },
36
        { :controller => 'watchers', :action => 'destroy' }
37
      )
38
    assert_routing(
39
        { :method => 'get', :path => "/watchers/autocomplete_for_user" },
40
        { :controller => 'watchers', :action => 'autocomplete_for_user' }
41
      )
42
    assert_routing(
43
        { :method => 'post', :path => "/watchers/watch" },
44
        { :controller => 'watchers', :action => 'watch' }
45
      )
46
    assert_routing(
47
        { :method => 'post', :path => "/watchers/unwatch" },
48
        { :controller => 'watchers', :action => 'unwatch' }
49
      )
50
  end
51
end
.svn/pristine/32/32bb68107ce90e09885cb5aa647d1b1b670aefe9.svn-base
1
<%= board_breadcrumb(@message) %>
2

  
3
<div class="contextual">
4
    <%= watcher_tag(@topic, User.current) %>
5
    <%= link_to(
6
          l(:button_quote),
7
          {:action => 'quote', :id => @topic},
8
          :remote => true,
9
          :method => 'get',
10
          :class => 'icon icon-comment',
11
          :remote => true) if !@topic.locked? && authorize_for('messages', 'reply') %>
12
    <%= link_to(
13
          l(:button_edit),
14
          {:action => 'edit', :id => @topic},
15
          :class => 'icon icon-edit'
16
        ) if @message.editable_by?(User.current) %>
17
    <%= link_to(
18
          l(:button_delete),
19
          {:action => 'destroy', :id => @topic},
20
          :method => :post,
21
          :data => {:confirm => l(:text_are_you_sure)},
22
          :class => 'icon icon-del'
23
         ) if @message.destroyable_by?(User.current) %>
24
</div>
25

  
26
<h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
27

  
28
<div class="message">
29
<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
30
<div class="wiki">
31
<%= textilizable(@topic, :content) %>
32
</div>
33
<%= link_to_attachments @topic, :author => false %>
34
</div>
35
<br />
36

  
37
<% unless @replies.empty? %>
38
<h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
39
<% @replies.each do |message| %>
40
  <div class="message reply" id="<%= "message-#{message.id}" %>">
41
    <div class="contextual">
42
      <%= link_to(
43
            image_tag('comment.png'),
44
            {:action => 'quote', :id => message},
45
            :remote => true,
46
            :method => 'get',
47
            :title => l(:button_quote)) if !@topic.locked? && authorize_for('messages', 'reply') %>
48
      <%= link_to(
49
            image_tag('edit.png'),
50
            {:action => 'edit', :id => message},
51
            :title => l(:button_edit)
52
          ) if message.editable_by?(User.current) %>
53
      <%= link_to(
54
            image_tag('delete.png'),
55
            {:action => 'destroy', :id => message},
56
            :method => :post,
57
            :data => {:confirm => l(:text_are_you_sure)},
58
            :title => l(:button_delete)
59
          ) if message.destroyable_by?(User.current) %>
60
    </div>
61
  <h4>
62
    <%= avatar(message.author, :size => "24") %>
63
    <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :r => message, :anchor => "message-#{message.id}" } %>
64
    -
65
    <%= authoring message.created_on, message.author %>
66
  </h4>
67
  <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
68
  <%= link_to_attachments message, :author => false %>
69
  </div>
70
<% end %>
71
<p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
72
<% end %>
73

  
74
<% if !@topic.locked? && authorize_for('messages', 'reply') %>
75
<p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
76
<div id="reply" style="display:none;">
77
<%= form_for @reply, :as => :reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
78
  <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
79
  <%= submit_tag l(:button_submit) %>
80
  <%= preview_link({:controller => 'messages', :action => 'preview', :board_id => @board}, 'message-form') %>
81
<% end %>
82
<div id="preview" class="wiki"></div>
83
</div>
84
<% end %>
85

  
86
<% html_title @topic.subject %>
.svn/pristine/32/32c52319e33e839952a511178f3d013ae8279f9b.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module Helpers
20

  
21
    # Simple class to compute the start and end dates of a calendar
22
    class Calendar
23
      include Redmine::I18n
24
      attr_reader :startdt, :enddt
25

  
26
      def initialize(date, lang = current_language, period = :month)
27
        @date = date
28
        @events = []
29
        @ending_events_by_days = {}
30
        @starting_events_by_days = {}
31
        set_language_if_valid lang
32
        case period
33
        when :month
34
          @startdt = Date.civil(date.year, date.month, 1)
35
          @enddt = (@startdt >> 1)-1
36
          # starts from the first day of the week
37
          @startdt = @startdt - (@startdt.cwday - first_wday)%7
38
          # ends on the last day of the week
39
          @enddt = @enddt + (last_wday - @enddt.cwday)%7
40
        when :week
41
          @startdt = date - (date.cwday - first_wday)%7
42
          @enddt = date + (last_wday - date.cwday)%7
43
        else
44
          raise 'Invalid period'
45
        end
46
      end
47

  
48
      # Sets calendar events
49
      def events=(events)
50
        @events = events
51
        @ending_events_by_days = @events.group_by {|event| event.due_date}
52
        @starting_events_by_days = @events.group_by {|event| event.start_date}
53
      end
54

  
55
      # Returns events for the given day
56
      def events_on(day)
57
        ((@ending_events_by_days[day] || []) + (@starting_events_by_days[day] || [])).uniq
58
      end
59

  
60
      # Calendar current month
61
      def month
62
        @date.month
63
      end
64

  
65
      # Return the first day of week
66
      # 1 = Monday ... 7 = Sunday
67
      def first_wday
68
        case Setting.start_of_week.to_i
69
        when 1
70
          @first_dow ||= (1 - 1)%7 + 1
71
        when 6
72
          @first_dow ||= (6 - 1)%7 + 1
73
        when 7
74
          @first_dow ||= (7 - 1)%7 + 1
75
        else
76
          @first_dow ||= (l(:general_first_day_of_week).to_i - 1)%7 + 1
77
        end
78
      end
79

  
80
      def last_wday
81
        @last_dow ||= (first_wday + 5)%7 + 1
82
      end
83
    end
84
  end
85
end
.svn/pristine/32/32d36ad9d8096df93d00980c0d2786c39a657ed2.svn-base
1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

  
18
module Redmine
19
  module WikiFormatting
20
    module Macros
21
      module Definitions
22
        # Returns true if +name+ is the name of an existing macro
23
        def macro_exists?(name)
24
          Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
25
        end
26

  
27
        def exec_macro(name, obj, args, text)
28
          macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
29
          return unless macro_options
30

  
31
          method_name = "macro_#{name}"
32
          unless macro_options[:parse_args] == false
33
            args = args.split(',').map(&:strip)
34
          end
35

  
36
          begin
37
            if self.class.instance_method(method_name).arity == 3
38
              send(method_name, obj, args, text)
39
            elsif text
40
              raise "This macro does not accept a block of text"
41
            else
42
              send(method_name, obj, args)
43
            end
44
          rescue => e
45
            "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
46
          end
47
        end
48

  
49
        def extract_macro_options(args, *keys)
50
          options = {}
51
          while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
52
            options[$1.downcase.to_sym] = $2
53
            args.pop
54
          end
55
          return [args, options]
56
        end
57
      end
58

  
59
      @@available_macros = {}
60
      mattr_accessor :available_macros
61

  
62
      class << self
63
        # Plugins can use this method to define new macros:
64
        #
65
        #   Redmine::WikiFormatting::Macros.register do
66
        #     desc "This is my macro"
67
        #     macro :my_macro do |obj, args|
68
        #       "My macro output"
69
        #     end
70
        #   
71
        #     desc "This is my macro that accepts a block of text"
72
        #     macro :my_macro do |obj, args, text|
73
        #       "My macro output"
74
        #     end
75
        #   end
76
        def register(&block)
77
          class_eval(&block) if block_given?
78
        end
79

  
80
        # Defines a new macro with the given name, options and block.
81
        #
82
        # Options:
83
        # * :desc - A description of the macro
84
        # * :parse_args => false - Disables arguments parsing (the whole arguments 
85
        #   string is passed to the macro)
86
        #
87
        # Macro blocks accept 2 or 3 arguments:
88
        # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
89
        # * args: macro arguments
90
        # * text: the block of text given to the macro (should be present only if the
91
        #   macro accepts a block of text). text is a String or nil if the macro is
92
        #   invoked without a block of text.  
93
        #
94
        # Examples:
95
        # By default, when the macro is invoked, the coma separated list of arguments
96
        # is split and passed to the macro block as an array. If no argument is given
97
        # the macro will be invoked with an empty array:
98
        #
99
        #   macro :my_macro do |obj, args|
100
        #     # args is an array
101
        #     # and this macro do not accept a block of text
102
        #   end
103
        #
104
        # You can disable arguments spliting with the :parse_args => false option. In
105
        # this case, the full string of arguments is passed to the macro:
106
        #
107
        #   macro :my_macro, :parse_args => false do |obj, args|
108
        #     # args is a string
109
        #   end
110
        #
111
        # Macro can optionally accept a block of text:
112
        #
113
        #   macro :my_macro do |obj, args, text|
114
        #     # this macro accepts a block of text
115
        #   end
116
        #
117
        # Macros are invoked in formatted text using double curly brackets. Arguments
118
        # must be enclosed in parenthesis if any. A new line after the macro name or the
119
        # arguments starts the block of text that will be passe to the macro (invoking
120
        # a macro that do not accept a block of text with some text will fail).
121
        # Examples:
122
        #
123
        #   No arguments:
124
        #   {{my_macro}}
125
        #
126
        #   With arguments:
127
        #   {{my_macro(arg1, arg2)}}
128
        #
129
        #   With a block of text:
130
        #   {{my_macro
131
        #   multiple lines
132
        #   of text
133
        #   }}
134
        #
135
        #   With arguments and a block of text
136
        #   {{my_macro(arg1, arg2)
137
        #   multiple lines
138
        #   of text
139
        #   }}
140
        #
141
        # If a block of text is given, the closing tag }} must be at the start of a new line.
142
        def macro(name, options={}, &block)
143
          options.assert_valid_keys(:desc, :parse_args)
144
          unless name.to_s.match(/\A\w+\z/)
145
            raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
146
          end
147
          unless block_given?
148
            raise "Can not create a macro without a block!"
149
          end
150
          name = name.to_s.downcase.to_sym
151
          available_macros[name] = {:desc => @@desc || ''}.merge(options)
152
          @@desc = nil
153
          Definitions.send :define_method, "macro_#{name}", &block
154
        end
155

  
156
        # Sets description for the next macro to be defined
157
        def desc(txt)
158
          @@desc = txt
159
        end
160
      end
161

  
162
      # Builtin macros
163
      desc "Sample macro."
164
      macro :hello_world do |obj, args, text|
165
        h("Hello world! Object: #{obj.class.name}, " + 
166
          (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
167
          " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
168
        )
169
      end
170

  
171
      desc "Displays a list of all available macros, including description if available."
172
      macro :macro_list do |obj, args|
173
        out = ''.html_safe
174
        @@available_macros.each do |macro, options|
175
          out << content_tag('dt', content_tag('code', macro.to_s))
176
          out << content_tag('dd', textilizable(options[:desc]))
177
        end
178
        content_tag('dl', out)
179
      end
180

  
181
      desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
182
             "  !{{child_pages}} -- can be used from a wiki page only\n" +
183
             "  !{{child_pages(depth=2)}} -- display 2 levels nesting only\n"
184
             "  !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
185
             "  !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
186
      macro :child_pages do |obj, args|
187
        args, options = extract_macro_options(args, :parent, :depth)
188
        options[:depth] = options[:depth].to_i if options[:depth].present?
189

  
190
        page = nil
191
        if args.size > 0
192
          page = Wiki.find_page(args.first.to_s, :project => @project)
193
        elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
194
          page = obj.page
195
        else
196
          raise 'With no argument, this macro can be called from wiki pages only.'
197
        end
198
        raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
199
        pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
200
        render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
201
      end
202

  
203
      desc "Include a wiki page. Example:\n\n  !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n  !{{include(projectname:Foo)}}"
204
      macro :include do |obj, args|
205
        page = Wiki.find_page(args.first.to_s, :project => @project)
206
        raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
207
        @included_wiki_pages ||= []
208
        raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
209
        @included_wiki_pages << page.title
210
        out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
211
        @included_wiki_pages.pop
212
        out
213
      end
214

  
215
      desc "Inserts of collapsed block of text. Example:\n\n  {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}"
216
      macro :collapse do |obj, args, text|
217
        html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
218
        show_label = args[0] || l(:button_show)
219
        hide_label = args[1] || args[0] || l(:button_hide)
220
        js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
221
        out = ''.html_safe
222
        out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
223
        out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
224
        out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
225
        out
226
      end
227

  
228
      desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
229
      macro :thumbnail do |obj, args|
230
        args, options = extract_macro_options(args, :size, :title)
231
        filename = args.first
232
        raise 'Filename required' unless filename.present?
233
        size = options[:size]
234
        raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
235
        size = size.to_i
236
        size = nil unless size > 0
237
        if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
238
          title = options[:title] || attachment.title
239
          img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
240
          link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
241
        else
242
          raise "Attachment #{filename} not found"
243
        end
244
      end
245
    end
246
  end
247
end

Also available in: Unified diff