Revision 1297:0a574315af3e .svn/pristine/32
| .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