##// END OF EJS Templates
Merged r2168 to r2171 from trunk....
Jean-Philippe Lang -
r2170:733987fbb6e4
parent child
Show More
@@ -1,92 +1,92
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AdminController < ApplicationController
18 class AdminController < ApplicationController
19 before_filter :require_admin
19 before_filter :require_admin
20
20
21 helper :sort
21 helper :sort
22 include SortHelper
22 include SortHelper
23
23
24 def index
24 def index
25 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
25 @no_configuration_data = Redmine::DefaultData::Loader::no_data?
26 end
26 end
27
27
28 def projects
28 def projects
29 sort_init 'name', 'asc'
29 sort_init 'name', 'asc'
30 sort_update
30 sort_update %w(name is_public created_on)
31
31
32 @status = params[:status] ? params[:status].to_i : 1
32 @status = params[:status] ? params[:status].to_i : 1
33 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
33 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
34
34
35 unless params[:name].blank?
35 unless params[:name].blank?
36 name = "%#{params[:name].strip.downcase}%"
36 name = "%#{params[:name].strip.downcase}%"
37 c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
37 c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
38 end
38 end
39
39
40 @project_count = Project.count(:conditions => c.conditions)
40 @project_count = Project.count(:conditions => c.conditions)
41 @project_pages = Paginator.new self, @project_count,
41 @project_pages = Paginator.new self, @project_count,
42 per_page_option,
42 per_page_option,
43 params['page']
43 params['page']
44 @projects = Project.find :all, :order => sort_clause,
44 @projects = Project.find :all, :order => sort_clause,
45 :conditions => c.conditions,
45 :conditions => c.conditions,
46 :limit => @project_pages.items_per_page,
46 :limit => @project_pages.items_per_page,
47 :offset => @project_pages.current.offset
47 :offset => @project_pages.current.offset
48
48
49 render :action => "projects", :layout => false if request.xhr?
49 render :action => "projects", :layout => false if request.xhr?
50 end
50 end
51
51
52 def plugins
52 def plugins
53 @plugins = Redmine::Plugin.all
53 @plugins = Redmine::Plugin.all
54 end
54 end
55
55
56 # Loads the default configuration
56 # Loads the default configuration
57 # (roles, trackers, statuses, workflow, enumerations)
57 # (roles, trackers, statuses, workflow, enumerations)
58 def default_configuration
58 def default_configuration
59 if request.post?
59 if request.post?
60 begin
60 begin
61 Redmine::DefaultData::Loader::load(params[:lang])
61 Redmine::DefaultData::Loader::load(params[:lang])
62 flash[:notice] = l(:notice_default_data_loaded)
62 flash[:notice] = l(:notice_default_data_loaded)
63 rescue Exception => e
63 rescue Exception => e
64 flash[:error] = l(:error_can_t_load_default_data, e.message)
64 flash[:error] = l(:error_can_t_load_default_data, e.message)
65 end
65 end
66 end
66 end
67 redirect_to :action => 'index'
67 redirect_to :action => 'index'
68 end
68 end
69
69
70 def test_email
70 def test_email
71 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
71 raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
72 # Force ActionMailer to raise delivery errors so we can catch it
72 # Force ActionMailer to raise delivery errors so we can catch it
73 ActionMailer::Base.raise_delivery_errors = true
73 ActionMailer::Base.raise_delivery_errors = true
74 begin
74 begin
75 @test = Mailer.deliver_test(User.current)
75 @test = Mailer.deliver_test(User.current)
76 flash[:notice] = l(:notice_email_sent, User.current.mail)
76 flash[:notice] = l(:notice_email_sent, User.current.mail)
77 rescue Exception => e
77 rescue Exception => e
78 flash[:error] = l(:notice_email_error, e.message)
78 flash[:error] = l(:notice_email_error, e.message)
79 end
79 end
80 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
80 ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
81 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
81 redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
82 end
82 end
83
83
84 def info
84 def info
85 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
85 @db_adapter_name = ActiveRecord::Base.connection.adapter_name
86 @flags = {
86 @flags = {
87 :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
87 :default_admin_changed => User.find(:first, :conditions => ["login=? and hashed_password=?", 'admin', User.hash_password('admin')]).nil?,
88 :file_repository_writable => File.writable?(Attachment.storage_path),
88 :file_repository_writable => File.writable?(Attachment.storage_path),
89 :rmagick_available => Object.const_defined?(:Magick)
89 :rmagick_available => Object.const_defined?(:Magick)
90 }
90 }
91 end
91 end
92 end
92 end
@@ -1,85 +1,87
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class BoardsController < ApplicationController
18 class BoardsController < ApplicationController
19 before_filter :find_project, :authorize
19 before_filter :find_project, :authorize
20
20
21 helper :messages
21 helper :messages
22 include MessagesHelper
22 include MessagesHelper
23 helper :sort
23 helper :sort
24 include SortHelper
24 include SortHelper
25 helper :watchers
25 helper :watchers
26 include WatchersHelper
26 include WatchersHelper
27
27
28 def index
28 def index
29 @boards = @project.boards
29 @boards = @project.boards
30 # show the board if there is only one
30 # show the board if there is only one
31 if @boards.size == 1
31 if @boards.size == 1
32 @board = @boards.first
32 @board = @boards.first
33 show
33 show
34 end
34 end
35 end
35 end
36
36
37 def show
37 def show
38 sort_init "#{Message.table_name}.updated_on", "desc"
38 sort_init 'updated_on', 'desc'
39 sort_update
39 sort_update 'created_on' => "#{Message.table_name}.created_on",
40 'replies' => "#{Message.table_name}.replies_count",
41 'updated_on' => "#{Message.table_name}.updated_on"
40
42
41 @topic_count = @board.topics.count
43 @topic_count = @board.topics.count
42 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
44 @topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
43 @topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
45 @topics = @board.topics.find :all, :order => "#{Message.table_name}.sticky DESC, #{sort_clause}",
44 :include => [:author, {:last_reply => :author}],
46 :include => [:author, {:last_reply => :author}],
45 :limit => @topic_pages.items_per_page,
47 :limit => @topic_pages.items_per_page,
46 :offset => @topic_pages.current.offset
48 :offset => @topic_pages.current.offset
47 render :action => 'show', :layout => !request.xhr?
49 render :action => 'show', :layout => !request.xhr?
48 end
50 end
49
51
50 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
52 verify :method => :post, :only => [ :destroy ], :redirect_to => { :action => :index }
51
53
52 def new
54 def new
53 @board = Board.new(params[:board])
55 @board = Board.new(params[:board])
54 @board.project = @project
56 @board.project = @project
55 if request.post? && @board.save
57 if request.post? && @board.save
56 flash[:notice] = l(:notice_successful_create)
58 flash[:notice] = l(:notice_successful_create)
57 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
59 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
58 end
60 end
59 end
61 end
60
62
61 def edit
63 def edit
62 if request.post? && @board.update_attributes(params[:board])
64 if request.post? && @board.update_attributes(params[:board])
63 case params[:position]
65 case params[:position]
64 when 'highest'; @board.move_to_top
66 when 'highest'; @board.move_to_top
65 when 'higher'; @board.move_higher
67 when 'higher'; @board.move_higher
66 when 'lower'; @board.move_lower
68 when 'lower'; @board.move_lower
67 when 'lowest'; @board.move_to_bottom
69 when 'lowest'; @board.move_to_bottom
68 end if params[:position]
70 end if params[:position]
69 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
71 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
70 end
72 end
71 end
73 end
72
74
73 def destroy
75 def destroy
74 @board.destroy
76 @board.destroy
75 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
77 redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
76 end
78 end
77
79
78 private
80 private
79 def find_project
81 def find_project
80 @project = Project.find(params[:project_id])
82 @project = Project.find(params[:project_id])
81 @board = @project.boards.find(params[:id]) if params[:id]
83 @board = @project.boards.find(params[:id]) if params[:id]
82 rescue ActiveRecord::RecordNotFound
84 rescue ActiveRecord::RecordNotFound
83 render_404
85 render_404
84 end
86 end
85 end
87 end
@@ -1,493 +1,495
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 menu_item :new_issue, :only => :new
19 menu_item :new_issue, :only => :new
20
20
21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
21 before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
23 before_filter :find_project, :only => [:new, :update_form, :preview]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
24 before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :update_form, :context_menu]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
25 before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
26 accept_key_auth :index, :changes
26 accept_key_auth :index, :changes
27
27
28 helper :journals
28 helper :journals
29 helper :projects
29 helper :projects
30 include ProjectsHelper
30 include ProjectsHelper
31 helper :custom_fields
31 helper :custom_fields
32 include CustomFieldsHelper
32 include CustomFieldsHelper
33 helper :ifpdf
33 helper :ifpdf
34 include IfpdfHelper
34 include IfpdfHelper
35 helper :issue_relations
35 helper :issue_relations
36 include IssueRelationsHelper
36 include IssueRelationsHelper
37 helper :watchers
37 helper :watchers
38 include WatchersHelper
38 include WatchersHelper
39 helper :attachments
39 helper :attachments
40 include AttachmentsHelper
40 include AttachmentsHelper
41 helper :queries
41 helper :queries
42 helper :sort
42 helper :sort
43 include SortHelper
43 include SortHelper
44 include IssuesHelper
44 include IssuesHelper
45 helper :timelog
45 helper :timelog
46
46
47 def index
47 def index
48 sort_init "#{Issue.table_name}.id", "desc"
49 sort_update
50 retrieve_query
48 retrieve_query
49 sort_init 'id', 'desc'
50 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
51
51 if @query.valid?
52 if @query.valid?
52 limit = per_page_option
53 limit = per_page_option
53 respond_to do |format|
54 respond_to do |format|
54 format.html { }
55 format.html { }
55 format.atom { }
56 format.atom { }
56 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.csv { limit = Setting.issues_export_limit.to_i }
57 format.pdf { limit = Setting.issues_export_limit.to_i }
58 format.pdf { limit = Setting.issues_export_limit.to_i }
58 end
59 end
59 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
60 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
61 @issues = Issue.find :all, :order => sort_clause,
62 @issues = Issue.find :all, :order => sort_clause,
62 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
63 :conditions => @query.statement,
64 :conditions => @query.statement,
64 :limit => limit,
65 :limit => limit,
65 :offset => @issue_pages.current.offset
66 :offset => @issue_pages.current.offset
66 respond_to do |format|
67 respond_to do |format|
67 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
68 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
69 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.csv { send_data(issues_to_csv(@issues, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') }
70 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 format.pdf { send_data(render(:template => 'issues/index.rfpdf', :layout => false), :type => 'application/pdf', :filename => 'export.pdf') }
71 end
72 end
72 else
73 else
73 # Send html if the query is not valid
74 # Send html if the query is not valid
74 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
75 end
76 end
76 rescue ActiveRecord::RecordNotFound
77 rescue ActiveRecord::RecordNotFound
77 render_404
78 render_404
78 end
79 end
79
80
80 def changes
81 def changes
81 sort_init "#{Issue.table_name}.id", "desc"
82 sort_update
83 retrieve_query
82 retrieve_query
83 sort_init 'id', 'desc'
84 sort_update({'id' => "#{Issue.table_name}.id"}.merge(@query.columns.inject({}) {|h, c| h[c.name.to_s] = c.sortable; h}))
85
84 if @query.valid?
86 if @query.valid?
85 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
87 @journals = Journal.find :all, :include => [ :details, :user, {:issue => [:project, :author, :tracker, :status]} ],
86 :conditions => @query.statement,
88 :conditions => @query.statement,
87 :limit => 25,
89 :limit => 25,
88 :order => "#{Journal.table_name}.created_on DESC"
90 :order => "#{Journal.table_name}.created_on DESC"
89 end
91 end
90 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
92 @title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
91 render :layout => false, :content_type => 'application/atom+xml'
93 render :layout => false, :content_type => 'application/atom+xml'
92 rescue ActiveRecord::RecordNotFound
94 rescue ActiveRecord::RecordNotFound
93 render_404
95 render_404
94 end
96 end
95
97
96 def show
98 def show
97 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
99 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
98 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.reverse! if User.current.wants_comments_in_reverse_order?
101 @journals.reverse! if User.current.wants_comments_in_reverse_order?
100 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
102 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
101 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
103 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
102 @priorities = Enumeration::get_values('IPRI')
104 @priorities = Enumeration::get_values('IPRI')
103 @time_entry = TimeEntry.new
105 @time_entry = TimeEntry.new
104 respond_to do |format|
106 respond_to do |format|
105 format.html { render :template => 'issues/show.rhtml' }
107 format.html { render :template => 'issues/show.rhtml' }
106 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
108 format.atom { render :action => 'changes', :layout => false, :content_type => 'application/atom+xml' }
107 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
109 format.pdf { send_data(render(:template => 'issues/show.rfpdf', :layout => false), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
108 end
110 end
109 end
111 end
110
112
111 # Add a new issue
113 # Add a new issue
112 # The new issue will be created from an existing one if copy_from parameter is given
114 # The new issue will be created from an existing one if copy_from parameter is given
113 def new
115 def new
114 @issue = Issue.new
116 @issue = Issue.new
115 @issue.copy_from(params[:copy_from]) if params[:copy_from]
117 @issue.copy_from(params[:copy_from]) if params[:copy_from]
116 @issue.project = @project
118 @issue.project = @project
117 # Tracker must be set before custom field values
119 # Tracker must be set before custom field values
118 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
120 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
119 if @issue.tracker.nil?
121 if @issue.tracker.nil?
120 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
122 flash.now[:error] = 'No tracker is associated to this project. Please check the Project settings.'
121 render :nothing => true, :layout => true
123 render :nothing => true, :layout => true
122 return
124 return
123 end
125 end
124 @issue.attributes = params[:issue]
126 @issue.attributes = params[:issue]
125 @issue.author = User.current
127 @issue.author = User.current
126
128
127 default_status = IssueStatus.default
129 default_status = IssueStatus.default
128 unless default_status
130 unless default_status
129 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
131 flash.now[:error] = 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
130 render :nothing => true, :layout => true
132 render :nothing => true, :layout => true
131 return
133 return
132 end
134 end
133 @issue.status = default_status
135 @issue.status = default_status
134 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
136 @allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
135
137
136 if request.get? || request.xhr?
138 if request.get? || request.xhr?
137 @issue.start_date ||= Date.today
139 @issue.start_date ||= Date.today
138 else
140 else
139 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
141 requested_status = IssueStatus.find_by_id(params[:issue][:status_id])
140 # Check that the user is allowed to apply the requested status
142 # Check that the user is allowed to apply the requested status
141 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
143 @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
142 if @issue.save
144 if @issue.save
143 attach_files(@issue, params[:attachments])
145 attach_files(@issue, params[:attachments])
144 flash[:notice] = l(:notice_successful_create)
146 flash[:notice] = l(:notice_successful_create)
145 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
147 Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
146 redirect_to :controller => 'issues', :action => 'show', :id => @issue
148 redirect_to :controller => 'issues', :action => 'show', :id => @issue
147 return
149 return
148 end
150 end
149 end
151 end
150 @priorities = Enumeration::get_values('IPRI')
152 @priorities = Enumeration::get_values('IPRI')
151 render :layout => !request.xhr?
153 render :layout => !request.xhr?
152 end
154 end
153
155
154 # Attributes that can be updated on workflow transition (without :edit permission)
156 # Attributes that can be updated on workflow transition (without :edit permission)
155 # TODO: make it configurable (at least per role)
157 # TODO: make it configurable (at least per role)
156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
158 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157
159
158 def edit
160 def edit
159 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
161 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
160 @priorities = Enumeration::get_values('IPRI')
162 @priorities = Enumeration::get_values('IPRI')
161 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
163 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
162 @time_entry = TimeEntry.new
164 @time_entry = TimeEntry.new
163
165
164 @notes = params[:notes]
166 @notes = params[:notes]
165 journal = @issue.init_journal(User.current, @notes)
167 journal = @issue.init_journal(User.current, @notes)
166 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
168 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
167 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
169 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
168 attrs = params[:issue].dup
170 attrs = params[:issue].dup
169 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
171 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
170 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
172 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
171 @issue.attributes = attrs
173 @issue.attributes = attrs
172 end
174 end
173
175
174 if request.post?
176 if request.post?
175 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
177 @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
176 @time_entry.attributes = params[:time_entry]
178 @time_entry.attributes = params[:time_entry]
177 attachments = attach_files(@issue, params[:attachments])
179 attachments = attach_files(@issue, params[:attachments])
178 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
180 attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
179
181
180 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
182 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => @time_entry, :journal => journal})
181
183
182 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
184 if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
183 # Log spend time
185 # Log spend time
184 if current_role.allowed_to?(:log_time)
186 if current_role.allowed_to?(:log_time)
185 @time_entry.save
187 @time_entry.save
186 end
188 end
187 if !journal.new_record?
189 if !journal.new_record?
188 # Only send notification if something was actually changed
190 # Only send notification if something was actually changed
189 flash[:notice] = l(:notice_successful_update)
191 flash[:notice] = l(:notice_successful_update)
190 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
192 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
191 end
193 end
192 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
194 redirect_to(params[:back_to] || {:action => 'show', :id => @issue})
193 end
195 end
194 end
196 end
195 rescue ActiveRecord::StaleObjectError
197 rescue ActiveRecord::StaleObjectError
196 # Optimistic locking exception
198 # Optimistic locking exception
197 flash.now[:error] = l(:notice_locking_conflict)
199 flash.now[:error] = l(:notice_locking_conflict)
198 end
200 end
199
201
200 def reply
202 def reply
201 journal = Journal.find(params[:journal_id]) if params[:journal_id]
203 journal = Journal.find(params[:journal_id]) if params[:journal_id]
202 if journal
204 if journal
203 user = journal.user
205 user = journal.user
204 text = journal.notes
206 text = journal.notes
205 else
207 else
206 user = @issue.author
208 user = @issue.author
207 text = @issue.description
209 text = @issue.description
208 end
210 end
209 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
211 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
210 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
212 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
211 render(:update) { |page|
213 render(:update) { |page|
212 page.<< "$('notes').value = \"#{content}\";"
214 page.<< "$('notes').value = \"#{content}\";"
213 page.show 'update'
215 page.show 'update'
214 page << "Form.Element.focus('notes');"
216 page << "Form.Element.focus('notes');"
215 page << "Element.scrollTo('update');"
217 page << "Element.scrollTo('update');"
216 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
218 page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
217 }
219 }
218 end
220 end
219
221
220 # Bulk edit a set of issues
222 # Bulk edit a set of issues
221 def bulk_edit
223 def bulk_edit
222 if request.post?
224 if request.post?
223 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
225 status = params[:status_id].blank? ? nil : IssueStatus.find_by_id(params[:status_id])
224 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
226 priority = params[:priority_id].blank? ? nil : Enumeration.find_by_id(params[:priority_id])
225 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
227 assigned_to = (params[:assigned_to_id].blank? || params[:assigned_to_id] == 'none') ? nil : User.find_by_id(params[:assigned_to_id])
226 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
228 category = (params[:category_id].blank? || params[:category_id] == 'none') ? nil : @project.issue_categories.find_by_id(params[:category_id])
227 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
229 fixed_version = (params[:fixed_version_id].blank? || params[:fixed_version_id] == 'none') ? nil : @project.versions.find_by_id(params[:fixed_version_id])
228
230
229 unsaved_issue_ids = []
231 unsaved_issue_ids = []
230 @issues.each do |issue|
232 @issues.each do |issue|
231 journal = issue.init_journal(User.current, params[:notes])
233 journal = issue.init_journal(User.current, params[:notes])
232 issue.priority = priority if priority
234 issue.priority = priority if priority
233 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
235 issue.assigned_to = assigned_to if assigned_to || params[:assigned_to_id] == 'none'
234 issue.category = category if category || params[:category_id] == 'none'
236 issue.category = category if category || params[:category_id] == 'none'
235 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
237 issue.fixed_version = fixed_version if fixed_version || params[:fixed_version_id] == 'none'
236 issue.start_date = params[:start_date] unless params[:start_date].blank?
238 issue.start_date = params[:start_date] unless params[:start_date].blank?
237 issue.due_date = params[:due_date] unless params[:due_date].blank?
239 issue.due_date = params[:due_date] unless params[:due_date].blank?
238 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
240 issue.done_ratio = params[:done_ratio] unless params[:done_ratio].blank?
239 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
241 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
240 # Don't save any change to the issue if the user is not authorized to apply the requested status
242 # Don't save any change to the issue if the user is not authorized to apply the requested status
241 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
243 if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
242 # Send notification for each issue (if changed)
244 # Send notification for each issue (if changed)
243 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
245 Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
244 else
246 else
245 # Keep unsaved issue ids to display them in flash error
247 # Keep unsaved issue ids to display them in flash error
246 unsaved_issue_ids << issue.id
248 unsaved_issue_ids << issue.id
247 end
249 end
248 end
250 end
249 if unsaved_issue_ids.empty?
251 if unsaved_issue_ids.empty?
250 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
252 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
251 else
253 else
252 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
254 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
253 end
255 end
254 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
256 redirect_to(params[:back_to] || {:controller => 'issues', :action => 'index', :project_id => @project})
255 return
257 return
256 end
258 end
257 # Find potential statuses the user could be allowed to switch issues to
259 # Find potential statuses the user could be allowed to switch issues to
258 @available_statuses = Workflow.find(:all, :include => :new_status,
260 @available_statuses = Workflow.find(:all, :include => :new_status,
259 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
261 :conditions => {:role_id => current_role.id}).collect(&:new_status).compact.uniq.sort
260 end
262 end
261
263
262 def move
264 def move
263 @allowed_projects = []
265 @allowed_projects = []
264 # find projects to which the user is allowed to move the issue
266 # find projects to which the user is allowed to move the issue
265 if User.current.admin?
267 if User.current.admin?
266 # admin is allowed to move issues to any active (visible) project
268 # admin is allowed to move issues to any active (visible) project
267 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
269 @allowed_projects = Project.find(:all, :conditions => Project.visible_by(User.current), :order => 'name')
268 else
270 else
269 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
271 User.current.memberships.each {|m| @allowed_projects << m.project if m.role.allowed_to?(:move_issues)}
270 end
272 end
271 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
273 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
272 @target_project ||= @project
274 @target_project ||= @project
273 @trackers = @target_project.trackers
275 @trackers = @target_project.trackers
274 if request.post?
276 if request.post?
275 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
277 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
276 unsaved_issue_ids = []
278 unsaved_issue_ids = []
277 @issues.each do |issue|
279 @issues.each do |issue|
278 issue.init_journal(User.current)
280 issue.init_journal(User.current)
279 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
281 unsaved_issue_ids << issue.id unless issue.move_to(@target_project, new_tracker)
280 end
282 end
281 if unsaved_issue_ids.empty?
283 if unsaved_issue_ids.empty?
282 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
284 flash[:notice] = l(:notice_successful_update) unless @issues.empty?
283 else
285 else
284 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
286 flash[:error] = l(:notice_failed_to_save_issues, unsaved_issue_ids.size, @issues.size, '#' + unsaved_issue_ids.join(', #'))
285 end
287 end
286 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
288 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
287 return
289 return
288 end
290 end
289 render :layout => false if request.xhr?
291 render :layout => false if request.xhr?
290 end
292 end
291
293
292 def destroy
294 def destroy
293 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
295 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
294 if @hours > 0
296 if @hours > 0
295 case params[:todo]
297 case params[:todo]
296 when 'destroy'
298 when 'destroy'
297 # nothing to do
299 # nothing to do
298 when 'nullify'
300 when 'nullify'
299 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
301 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
300 when 'reassign'
302 when 'reassign'
301 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
303 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
302 if reassign_to.nil?
304 if reassign_to.nil?
303 flash.now[:error] = l(:error_issue_not_found_in_project)
305 flash.now[:error] = l(:error_issue_not_found_in_project)
304 return
306 return
305 else
307 else
306 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
308 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
307 end
309 end
308 else
310 else
309 # display the destroy form
311 # display the destroy form
310 return
312 return
311 end
313 end
312 end
314 end
313 @issues.each(&:destroy)
315 @issues.each(&:destroy)
314 redirect_to :action => 'index', :project_id => @project
316 redirect_to :action => 'index', :project_id => @project
315 end
317 end
316
318
317 def destroy_attachment
319 def destroy_attachment
318 a = @issue.attachments.find(params[:attachment_id])
320 a = @issue.attachments.find(params[:attachment_id])
319 a.destroy
321 a.destroy
320 journal = @issue.init_journal(User.current)
322 journal = @issue.init_journal(User.current)
321 journal.details << JournalDetail.new(:property => 'attachment',
323 journal.details << JournalDetail.new(:property => 'attachment',
322 :prop_key => a.id,
324 :prop_key => a.id,
323 :old_value => a.filename)
325 :old_value => a.filename)
324 journal.save
326 journal.save
325 redirect_to :action => 'show', :id => @issue
327 redirect_to :action => 'show', :id => @issue
326 end
328 end
327
329
328 def gantt
330 def gantt
329 @gantt = Redmine::Helpers::Gantt.new(params)
331 @gantt = Redmine::Helpers::Gantt.new(params)
330 retrieve_query
332 retrieve_query
331 if @query.valid?
333 if @query.valid?
332 events = []
334 events = []
333 # Issues that have start and due dates
335 # Issues that have start and due dates
334 events += Issue.find(:all,
336 events += Issue.find(:all,
335 :order => "start_date, due_date",
337 :order => "start_date, due_date",
336 :include => [:tracker, :status, :assigned_to, :priority, :project],
338 :include => [:tracker, :status, :assigned_to, :priority, :project],
337 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
339 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
338 )
340 )
339 # Issues that don't have a due date but that are assigned to a version with a date
341 # Issues that don't have a due date but that are assigned to a version with a date
340 events += Issue.find(:all,
342 events += Issue.find(:all,
341 :order => "start_date, effective_date",
343 :order => "start_date, effective_date",
342 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
344 :include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
343 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
345 :conditions => ["(#{@query.statement}) AND (((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
344 )
346 )
345 # Versions
347 # Versions
346 events += Version.find(:all, :include => :project,
348 events += Version.find(:all, :include => :project,
347 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
349 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
348
350
349 @gantt.events = events
351 @gantt.events = events
350 end
352 end
351
353
352 respond_to do |format|
354 respond_to do |format|
353 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
355 format.html { render :template => "issues/gantt.rhtml", :layout => !request.xhr? }
354 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
356 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{@project.identifier}-gantt.png") } if @gantt.respond_to?('to_image')
355 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
357 format.pdf { send_data(render(:template => "issues/gantt.rfpdf", :layout => false), :type => 'application/pdf', :filename => "#{@project.nil? ? '' : "#{@project.identifier}-" }gantt.pdf") }
356 end
358 end
357 end
359 end
358
360
359 def calendar
361 def calendar
360 if params[:year] and params[:year].to_i > 1900
362 if params[:year] and params[:year].to_i > 1900
361 @year = params[:year].to_i
363 @year = params[:year].to_i
362 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
364 if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
363 @month = params[:month].to_i
365 @month = params[:month].to_i
364 end
366 end
365 end
367 end
366 @year ||= Date.today.year
368 @year ||= Date.today.year
367 @month ||= Date.today.month
369 @month ||= Date.today.month
368
370
369 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
371 @calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
370 retrieve_query
372 retrieve_query
371 if @query.valid?
373 if @query.valid?
372 events = []
374 events = []
373 events += Issue.find(:all,
375 events += Issue.find(:all,
374 :include => [:tracker, :status, :assigned_to, :priority, :project],
376 :include => [:tracker, :status, :assigned_to, :priority, :project],
375 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
377 :conditions => ["(#{@query.statement}) AND ((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
376 )
378 )
377 events += Version.find(:all, :include => :project,
379 events += Version.find(:all, :include => :project,
378 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
380 :conditions => ["(#{@query.project_statement}) AND effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
379
381
380 @calendar.events = events
382 @calendar.events = events
381 end
383 end
382
384
383 render :layout => false if request.xhr?
385 render :layout => false if request.xhr?
384 end
386 end
385
387
386 def context_menu
388 def context_menu
387 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
389 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
388 if (@issues.size == 1)
390 if (@issues.size == 1)
389 @issue = @issues.first
391 @issue = @issues.first
390 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
392 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
391 end
393 end
392 projects = @issues.collect(&:project).compact.uniq
394 projects = @issues.collect(&:project).compact.uniq
393 @project = projects.first if projects.size == 1
395 @project = projects.first if projects.size == 1
394
396
395 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
397 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
396 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
398 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
397 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
399 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
398 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
400 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
399 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
401 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
400 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
402 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
401 }
403 }
402 if @project
404 if @project
403 @assignables = @project.assignable_users
405 @assignables = @project.assignable_users
404 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
406 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
405 end
407 end
406
408
407 @priorities = Enumeration.get_values('IPRI').reverse
409 @priorities = Enumeration.get_values('IPRI').reverse
408 @statuses = IssueStatus.find(:all, :order => 'position')
410 @statuses = IssueStatus.find(:all, :order => 'position')
409 @back = request.env['HTTP_REFERER']
411 @back = request.env['HTTP_REFERER']
410
412
411 render :layout => false
413 render :layout => false
412 end
414 end
413
415
414 def update_form
416 def update_form
415 @issue = Issue.new(params[:issue])
417 @issue = Issue.new(params[:issue])
416 render :action => :new, :layout => false
418 render :action => :new, :layout => false
417 end
419 end
418
420
419 def preview
421 def preview
420 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
422 @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
421 @attachements = @issue.attachments if @issue
423 @attachements = @issue.attachments if @issue
422 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
424 @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
423 render :partial => 'common/preview'
425 render :partial => 'common/preview'
424 end
426 end
425
427
426 private
428 private
427 def find_issue
429 def find_issue
428 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
430 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
429 @project = @issue.project
431 @project = @issue.project
430 rescue ActiveRecord::RecordNotFound
432 rescue ActiveRecord::RecordNotFound
431 render_404
433 render_404
432 end
434 end
433
435
434 # Filter for bulk operations
436 # Filter for bulk operations
435 def find_issues
437 def find_issues
436 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
438 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
437 raise ActiveRecord::RecordNotFound if @issues.empty?
439 raise ActiveRecord::RecordNotFound if @issues.empty?
438 projects = @issues.collect(&:project).compact.uniq
440 projects = @issues.collect(&:project).compact.uniq
439 if projects.size == 1
441 if projects.size == 1
440 @project = projects.first
442 @project = projects.first
441 else
443 else
442 # TODO: let users bulk edit/move/destroy issues from different projects
444 # TODO: let users bulk edit/move/destroy issues from different projects
443 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
445 render_error 'Can not bulk edit/move/destroy issues from different projects' and return false
444 end
446 end
445 rescue ActiveRecord::RecordNotFound
447 rescue ActiveRecord::RecordNotFound
446 render_404
448 render_404
447 end
449 end
448
450
449 def find_project
451 def find_project
450 @project = Project.find(params[:project_id])
452 @project = Project.find(params[:project_id])
451 rescue ActiveRecord::RecordNotFound
453 rescue ActiveRecord::RecordNotFound
452 render_404
454 render_404
453 end
455 end
454
456
455 def find_optional_project
457 def find_optional_project
456 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
458 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
457 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
459 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
458 allowed ? true : deny_access
460 allowed ? true : deny_access
459 rescue ActiveRecord::RecordNotFound
461 rescue ActiveRecord::RecordNotFound
460 render_404
462 render_404
461 end
463 end
462
464
463 # Retrieve query from session or build a new query
465 # Retrieve query from session or build a new query
464 def retrieve_query
466 def retrieve_query
465 if !params[:query_id].blank?
467 if !params[:query_id].blank?
466 cond = "project_id IS NULL"
468 cond = "project_id IS NULL"
467 cond << " OR project_id = #{@project.id}" if @project
469 cond << " OR project_id = #{@project.id}" if @project
468 @query = Query.find(params[:query_id], :conditions => cond)
470 @query = Query.find(params[:query_id], :conditions => cond)
469 @query.project = @project
471 @query.project = @project
470 session[:query] = {:id => @query.id, :project_id => @query.project_id}
472 session[:query] = {:id => @query.id, :project_id => @query.project_id}
471 else
473 else
472 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
474 if params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
473 # Give it a name, required to be valid
475 # Give it a name, required to be valid
474 @query = Query.new(:name => "_")
476 @query = Query.new(:name => "_")
475 @query.project = @project
477 @query.project = @project
476 if params[:fields] and params[:fields].is_a? Array
478 if params[:fields] and params[:fields].is_a? Array
477 params[:fields].each do |field|
479 params[:fields].each do |field|
478 @query.add_filter(field, params[:operators][field], params[:values][field])
480 @query.add_filter(field, params[:operators][field], params[:values][field])
479 end
481 end
480 else
482 else
481 @query.available_filters.keys.each do |field|
483 @query.available_filters.keys.each do |field|
482 @query.add_short_filter(field, params[field]) if params[field]
484 @query.add_short_filter(field, params[field]) if params[field]
483 end
485 end
484 end
486 end
485 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
487 session[:query] = {:project_id => @query.project_id, :filters => @query.filters}
486 else
488 else
487 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
489 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
488 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
490 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters])
489 @query.project = @project
491 @query.project = @project
490 end
492 end
491 end
493 end
492 end
494 end
493 end
495 end
@@ -1,285 +1,289
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class ProjectsController < ApplicationController
18 class ProjectsController < ApplicationController
19 menu_item :overview
19 menu_item :overview
20 menu_item :activity, :only => :activity
20 menu_item :activity, :only => :activity
21 menu_item :roadmap, :only => :roadmap
21 menu_item :roadmap, :only => :roadmap
22 menu_item :files, :only => [:list_files, :add_file]
22 menu_item :files, :only => [:list_files, :add_file]
23 menu_item :settings, :only => :settings
23 menu_item :settings, :only => :settings
24 menu_item :issues, :only => [:changelog]
24 menu_item :issues, :only => [:changelog]
25
25
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
26 before_filter :find_project, :except => [ :index, :list, :add, :activity ]
27 before_filter :find_optional_project, :only => :activity
27 before_filter :find_optional_project, :only => :activity
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
28 before_filter :authorize, :except => [ :index, :list, :add, :archive, :unarchive, :destroy, :activity ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
29 before_filter :require_admin, :only => [ :add, :archive, :unarchive, :destroy ]
30 accept_key_auth :activity
30 accept_key_auth :activity
31
31
32 helper :sort
32 helper :sort
33 include SortHelper
33 include SortHelper
34 helper :custom_fields
34 helper :custom_fields
35 include CustomFieldsHelper
35 include CustomFieldsHelper
36 helper :ifpdf
36 helper :ifpdf
37 include IfpdfHelper
37 include IfpdfHelper
38 helper :issues
38 helper :issues
39 helper IssuesHelper
39 helper IssuesHelper
40 helper :queries
40 helper :queries
41 include QueriesHelper
41 include QueriesHelper
42 helper :repositories
42 helper :repositories
43 include RepositoriesHelper
43 include RepositoriesHelper
44 include ProjectsHelper
44 include ProjectsHelper
45
45
46 # Lists visible projects
46 # Lists visible projects
47 def index
47 def index
48 projects = Project.find :all,
48 projects = Project.find :all,
49 :conditions => Project.visible_by(User.current),
49 :conditions => Project.visible_by(User.current),
50 :include => :parent
50 :include => :parent
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @project_tree = projects.group_by {|p| p.parent || p}
53 @project_tree = projects.group_by {|p| p.parent || p}
54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
54 @project_tree.keys.each {|p| @project_tree[p] -= [p]}
55 }
55 }
56 format.atom {
56 format.atom {
57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
57 render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i),
58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
58 :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
59 }
59 }
60 end
60 end
61 end
61 end
62
62
63 # Add a new project
63 # Add a new project
64 def add
64 def add
65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
65 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
66 @trackers = Tracker.all
66 @trackers = Tracker.all
67 @root_projects = Project.find(:all,
67 @root_projects = Project.find(:all,
68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
68 :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
69 :order => 'name')
69 :order => 'name')
70 @project = Project.new(params[:project])
70 @project = Project.new(params[:project])
71 if request.get?
71 if request.get?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
72 @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
73 @project.trackers = Tracker.all
73 @project.trackers = Tracker.all
74 @project.is_public = Setting.default_projects_public?
74 @project.is_public = Setting.default_projects_public?
75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
75 @project.enabled_module_names = Redmine::AccessControl.available_project_modules
76 else
76 else
77 @project.enabled_module_names = params[:enabled_modules]
77 @project.enabled_module_names = params[:enabled_modules]
78 if @project.save
78 if @project.save
79 flash[:notice] = l(:notice_successful_create)
79 flash[:notice] = l(:notice_successful_create)
80 redirect_to :controller => 'admin', :action => 'projects'
80 redirect_to :controller => 'admin', :action => 'projects'
81 end
81 end
82 end
82 end
83 end
83 end
84
84
85 # Show @project
85 # Show @project
86 def show
86 def show
87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
87 @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role}
88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
88 @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current))
89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
89 @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
90 @trackers = @project.rolled_up_trackers
90 @trackers = @project.rolled_up_trackers
91
91
92 cond = @project.project_condition(Setting.display_subprojects_issues?)
92 cond = @project.project_condition(Setting.display_subprojects_issues?)
93 Issue.visible_by(User.current) do
93 Issue.visible_by(User.current) do
94 @open_issues_by_tracker = Issue.count(:group => :tracker,
94 @open_issues_by_tracker = Issue.count(:group => :tracker,
95 :include => [:project, :status, :tracker],
95 :include => [:project, :status, :tracker],
96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
96 :conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
97 @total_issues_by_tracker = Issue.count(:group => :tracker,
97 @total_issues_by_tracker = Issue.count(:group => :tracker,
98 :include => [:project, :status, :tracker],
98 :include => [:project, :status, :tracker],
99 :conditions => cond)
99 :conditions => cond)
100 end
100 end
101 TimeEntry.visible_by(User.current) do
101 TimeEntry.visible_by(User.current) do
102 @total_hours = TimeEntry.sum(:hours,
102 @total_hours = TimeEntry.sum(:hours,
103 :include => :project,
103 :include => :project,
104 :conditions => cond).to_f
104 :conditions => cond).to_f
105 end
105 end
106 @key = User.current.rss_key
106 @key = User.current.rss_key
107 end
107 end
108
108
109 def settings
109 def settings
110 @root_projects = Project.find(:all,
110 @root_projects = Project.find(:all,
111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
111 :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id],
112 :order => 'name')
112 :order => 'name')
113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
113 @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
114 @issue_category ||= IssueCategory.new
114 @issue_category ||= IssueCategory.new
115 @member ||= @project.members.new
115 @member ||= @project.members.new
116 @trackers = Tracker.all
116 @trackers = Tracker.all
117 @repository ||= @project.repository
117 @repository ||= @project.repository
118 @wiki ||= @project.wiki
118 @wiki ||= @project.wiki
119 end
119 end
120
120
121 # Edit @project
121 # Edit @project
122 def edit
122 def edit
123 if request.post?
123 if request.post?
124 @project.attributes = params[:project]
124 @project.attributes = params[:project]
125 if @project.save
125 if @project.save
126 flash[:notice] = l(:notice_successful_update)
126 flash[:notice] = l(:notice_successful_update)
127 redirect_to :action => 'settings', :id => @project
127 redirect_to :action => 'settings', :id => @project
128 else
128 else
129 settings
129 settings
130 render :action => 'settings'
130 render :action => 'settings'
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 def modules
135 def modules
136 @project.enabled_module_names = params[:enabled_modules]
136 @project.enabled_module_names = params[:enabled_modules]
137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
137 redirect_to :action => 'settings', :id => @project, :tab => 'modules'
138 end
138 end
139
139
140 def archive
140 def archive
141 @project.archive if request.post? && @project.active?
141 @project.archive if request.post? && @project.active?
142 redirect_to :controller => 'admin', :action => 'projects'
142 redirect_to :controller => 'admin', :action => 'projects'
143 end
143 end
144
144
145 def unarchive
145 def unarchive
146 @project.unarchive if request.post? && !@project.active?
146 @project.unarchive if request.post? && !@project.active?
147 redirect_to :controller => 'admin', :action => 'projects'
147 redirect_to :controller => 'admin', :action => 'projects'
148 end
148 end
149
149
150 # Delete @project
150 # Delete @project
151 def destroy
151 def destroy
152 @project_to_destroy = @project
152 @project_to_destroy = @project
153 if request.post? and params[:confirm]
153 if request.post? and params[:confirm]
154 @project_to_destroy.destroy
154 @project_to_destroy.destroy
155 redirect_to :controller => 'admin', :action => 'projects'
155 redirect_to :controller => 'admin', :action => 'projects'
156 end
156 end
157 # hide project in layout
157 # hide project in layout
158 @project = nil
158 @project = nil
159 end
159 end
160
160
161 # Add a new issue category to @project
161 # Add a new issue category to @project
162 def add_issue_category
162 def add_issue_category
163 @category = @project.issue_categories.build(params[:category])
163 @category = @project.issue_categories.build(params[:category])
164 if request.post? and @category.save
164 if request.post? and @category.save
165 respond_to do |format|
165 respond_to do |format|
166 format.html do
166 format.html do
167 flash[:notice] = l(:notice_successful_create)
167 flash[:notice] = l(:notice_successful_create)
168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
168 redirect_to :action => 'settings', :tab => 'categories', :id => @project
169 end
169 end
170 format.js do
170 format.js do
171 # IE doesn't support the replace_html rjs method for select box options
171 # IE doesn't support the replace_html rjs method for select box options
172 render(:update) {|page| page.replace "issue_category_id",
172 render(:update) {|page| page.replace "issue_category_id",
173 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
173 content_tag('select', '<option></option>' + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
174 }
174 }
175 end
175 end
176 end
176 end
177 end
177 end
178 end
178 end
179
179
180 # Add a new version to @project
180 # Add a new version to @project
181 def add_version
181 def add_version
182 @version = @project.versions.build(params[:version])
182 @version = @project.versions.build(params[:version])
183 if request.post? and @version.save
183 if request.post? and @version.save
184 flash[:notice] = l(:notice_successful_create)
184 flash[:notice] = l(:notice_successful_create)
185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
185 redirect_to :action => 'settings', :tab => 'versions', :id => @project
186 end
186 end
187 end
187 end
188
188
189 def add_file
189 def add_file
190 if request.post?
190 if request.post?
191 @version = @project.versions.find_by_id(params[:version_id])
191 @version = @project.versions.find_by_id(params[:version_id])
192 attachments = attach_files(@version, params[:attachments])
192 attachments = attach_files(@version, params[:attachments])
193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
193 Mailer.deliver_attachments_added(attachments) if !attachments.empty? && Setting.notified_events.include?('file_added')
194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
194 redirect_to :controller => 'projects', :action => 'list_files', :id => @project
195 end
195 end
196 @versions = @project.versions.sort
196 @versions = @project.versions.sort
197 end
197 end
198
198
199 def list_files
199 def list_files
200 sort_init "#{Attachment.table_name}.filename", "asc"
200 sort_init 'filename', 'asc'
201 sort_update
201 sort_update 'filename' => "#{Attachment.table_name}.filename",
202 'created_on' => "#{Attachment.table_name}.created_on",
203 'size' => "#{Attachment.table_name}.filesize",
204 'downloads' => "#{Attachment.table_name}.downloads"
205
202 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
206 @versions = @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
203 render :layout => !request.xhr?
207 render :layout => !request.xhr?
204 end
208 end
205
209
206 # Show changelog for @project
210 # Show changelog for @project
207 def changelog
211 def changelog
208 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
212 @trackers = @project.trackers.find(:all, :conditions => ["is_in_chlog=?", true], :order => 'position')
209 retrieve_selected_tracker_ids(@trackers)
213 retrieve_selected_tracker_ids(@trackers)
210 @versions = @project.versions.sort
214 @versions = @project.versions.sort
211 end
215 end
212
216
213 def roadmap
217 def roadmap
214 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
218 @trackers = @project.trackers.find(:all, :conditions => ["is_in_roadmap=?", true])
215 retrieve_selected_tracker_ids(@trackers)
219 retrieve_selected_tracker_ids(@trackers)
216 @versions = @project.versions.sort
220 @versions = @project.versions.sort
217 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
221 @versions = @versions.select {|v| !v.completed? } unless params[:completed]
218 end
222 end
219
223
220 def activity
224 def activity
221 @days = Setting.activity_days_default.to_i
225 @days = Setting.activity_days_default.to_i
222
226
223 if params[:from]
227 if params[:from]
224 begin; @date_to = params[:from].to_date + 1; rescue; end
228 begin; @date_to = params[:from].to_date + 1; rescue; end
225 end
229 end
226
230
227 @date_to ||= Date.today + 1
231 @date_to ||= Date.today + 1
228 @date_from = @date_to - @days
232 @date_from = @date_to - @days
229 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
233 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
230 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
234 @author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
231
235
232 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
236 @activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
233 :with_subprojects => @with_subprojects,
237 :with_subprojects => @with_subprojects,
234 :author => @author)
238 :author => @author)
235 @activity.scope_select {|t| !params["show_#{t}"].nil?}
239 @activity.scope_select {|t| !params["show_#{t}"].nil?}
236 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
240 @activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
237
241
238 events = @activity.events(@date_from, @date_to)
242 events = @activity.events(@date_from, @date_to)
239
243
240 respond_to do |format|
244 respond_to do |format|
241 format.html {
245 format.html {
242 @events_by_day = events.group_by(&:event_date)
246 @events_by_day = events.group_by(&:event_date)
243 render :layout => false if request.xhr?
247 render :layout => false if request.xhr?
244 }
248 }
245 format.atom {
249 format.atom {
246 title = l(:label_activity)
250 title = l(:label_activity)
247 if @author
251 if @author
248 title = @author.name
252 title = @author.name
249 elsif @activity.scope.size == 1
253 elsif @activity.scope.size == 1
250 title = l("label_#{@activity.scope.first.singularize}_plural")
254 title = l("label_#{@activity.scope.first.singularize}_plural")
251 end
255 end
252 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
256 render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
253 }
257 }
254 end
258 end
255
259
256 rescue ActiveRecord::RecordNotFound
260 rescue ActiveRecord::RecordNotFound
257 render_404
261 render_404
258 end
262 end
259
263
260 private
264 private
261 # Find project of id params[:id]
265 # Find project of id params[:id]
262 # if not found, redirect to project list
266 # if not found, redirect to project list
263 # Used as a before_filter
267 # Used as a before_filter
264 def find_project
268 def find_project
265 @project = Project.find(params[:id])
269 @project = Project.find(params[:id])
266 rescue ActiveRecord::RecordNotFound
270 rescue ActiveRecord::RecordNotFound
267 render_404
271 render_404
268 end
272 end
269
273
270 def find_optional_project
274 def find_optional_project
271 return true unless params[:id]
275 return true unless params[:id]
272 @project = Project.find(params[:id])
276 @project = Project.find(params[:id])
273 authorize
277 authorize
274 rescue ActiveRecord::RecordNotFound
278 rescue ActiveRecord::RecordNotFound
275 render_404
279 render_404
276 end
280 end
277
281
278 def retrieve_selected_tracker_ids(selectable_trackers)
282 def retrieve_selected_tracker_ids(selectable_trackers)
279 if ids = params[:tracker_ids]
283 if ids = params[:tracker_ids]
280 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
284 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
281 else
285 else
282 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
286 @selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
283 end
287 end
284 end
288 end
285 end
289 end
@@ -1,285 +1,290
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
20 before_filter :find_project, :authorize, :only => [:edit, :destroy]
21 before_filter :find_optional_project, :only => [:report, :details]
21 before_filter :find_optional_project, :only => [:report, :details]
22
22
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
23 verify :method => :post, :only => :destroy, :redirect_to => { :action => :details }
24
24
25 helper :sort
25 helper :sort
26 include SortHelper
26 include SortHelper
27 helper :issues
27 helper :issues
28 include TimelogHelper
28 include TimelogHelper
29 helper :custom_fields
29 helper :custom_fields
30 include CustomFieldsHelper
30 include CustomFieldsHelper
31
31
32 def report
32 def report
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
33 @available_criterias = { 'project' => {:sql => "#{TimeEntry.table_name}.project_id",
34 :klass => Project,
34 :klass => Project,
35 :label => :label_project},
35 :label => :label_project},
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
36 'version' => {:sql => "#{Issue.table_name}.fixed_version_id",
37 :klass => Version,
37 :klass => Version,
38 :label => :label_version},
38 :label => :label_version},
39 'category' => {:sql => "#{Issue.table_name}.category_id",
39 'category' => {:sql => "#{Issue.table_name}.category_id",
40 :klass => IssueCategory,
40 :klass => IssueCategory,
41 :label => :field_category},
41 :label => :field_category},
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
42 'member' => {:sql => "#{TimeEntry.table_name}.user_id",
43 :klass => User,
43 :klass => User,
44 :label => :label_member},
44 :label => :label_member},
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
45 'tracker' => {:sql => "#{Issue.table_name}.tracker_id",
46 :klass => Tracker,
46 :klass => Tracker,
47 :label => :label_tracker},
47 :label => :label_tracker},
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
48 'activity' => {:sql => "#{TimeEntry.table_name}.activity_id",
49 :klass => Enumeration,
49 :klass => Enumeration,
50 :label => :label_activity},
50 :label => :label_activity},
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
51 'issue' => {:sql => "#{TimeEntry.table_name}.issue_id",
52 :klass => Issue,
52 :klass => Issue,
53 :label => :label_issue}
53 :label => :label_issue}
54 }
54 }
55
55
56 # Add list and boolean custom fields as available criterias
56 # Add list and boolean custom fields as available criterias
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
57 custom_fields = (@project.nil? ? IssueCustomField.for_all : @project.all_issue_custom_fields)
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
58 custom_fields.select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
59 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
59 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'Issue' AND c.customized_id = #{Issue.table_name}.id)",
60 :format => cf.field_format,
60 :format => cf.field_format,
61 :label => cf.name}
61 :label => cf.name}
62 end if @project
62 end if @project
63
63
64 # Add list and boolean time entry custom fields
64 # Add list and boolean time entry custom fields
65 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
65 TimeEntryCustomField.find(:all).select {|cf| %w(list bool).include? cf.field_format }.each do |cf|
66 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
66 @available_criterias["cf_#{cf.id}"] = {:sql => "(SELECT c.value FROM #{CustomValue.table_name} c WHERE c.custom_field_id = #{cf.id} AND c.customized_type = 'TimeEntry' AND c.customized_id = #{TimeEntry.table_name}.id)",
67 :format => cf.field_format,
67 :format => cf.field_format,
68 :label => cf.name}
68 :label => cf.name}
69 end
69 end
70
70
71 @criterias = params[:criterias] || []
71 @criterias = params[:criterias] || []
72 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
72 @criterias = @criterias.select{|criteria| @available_criterias.has_key? criteria}
73 @criterias.uniq!
73 @criterias.uniq!
74 @criterias = @criterias[0,3]
74 @criterias = @criterias[0,3]
75
75
76 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
76 @columns = (params[:columns] && %w(year month week day).include?(params[:columns])) ? params[:columns] : 'month'
77
77
78 retrieve_date_range
78 retrieve_date_range
79
79
80 unless @criterias.empty?
80 unless @criterias.empty?
81 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
81 sql_select = @criterias.collect{|criteria| @available_criterias[criteria][:sql] + " AS " + criteria}.join(', ')
82 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
82 sql_group_by = @criterias.collect{|criteria| @available_criterias[criteria][:sql]}.join(', ')
83
83
84 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
84 sql = "SELECT #{sql_select}, tyear, tmonth, tweek, spent_on, SUM(hours) AS hours"
85 sql << " FROM #{TimeEntry.table_name}"
85 sql << " FROM #{TimeEntry.table_name}"
86 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
86 sql << " LEFT JOIN #{Issue.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id"
87 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
87 sql << " LEFT JOIN #{Project.table_name} ON #{TimeEntry.table_name}.project_id = #{Project.table_name}.id"
88 sql << " WHERE"
88 sql << " WHERE"
89 sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
89 sql << " (%s) AND" % @project.project_condition(Setting.display_subprojects_issues?) if @project
90 sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
90 sql << " (%s) AND" % Project.allowed_to_condition(User.current, :view_time_entries)
91 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
91 sql << " (spent_on BETWEEN '%s' AND '%s')" % [ActiveRecord::Base.connection.quoted_date(@from.to_time), ActiveRecord::Base.connection.quoted_date(@to.to_time)]
92 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
92 sql << " GROUP BY #{sql_group_by}, tyear, tmonth, tweek, spent_on"
93
93
94 @hours = ActiveRecord::Base.connection.select_all(sql)
94 @hours = ActiveRecord::Base.connection.select_all(sql)
95
95
96 @hours.each do |row|
96 @hours.each do |row|
97 case @columns
97 case @columns
98 when 'year'
98 when 'year'
99 row['year'] = row['tyear']
99 row['year'] = row['tyear']
100 when 'month'
100 when 'month'
101 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
101 row['month'] = "#{row['tyear']}-#{row['tmonth']}"
102 when 'week'
102 when 'week'
103 row['week'] = "#{row['tyear']}-#{row['tweek']}"
103 row['week'] = "#{row['tyear']}-#{row['tweek']}"
104 when 'day'
104 when 'day'
105 row['day'] = "#{row['spent_on']}"
105 row['day'] = "#{row['spent_on']}"
106 end
106 end
107 end
107 end
108
108
109 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
109 @total_hours = @hours.inject(0) {|s,k| s = s + k['hours'].to_f}
110
110
111 @periods = []
111 @periods = []
112 # Date#at_beginning_of_ not supported in Rails 1.2.x
112 # Date#at_beginning_of_ not supported in Rails 1.2.x
113 date_from = @from.to_time
113 date_from = @from.to_time
114 # 100 columns max
114 # 100 columns max
115 while date_from <= @to.to_time && @periods.length < 100
115 while date_from <= @to.to_time && @periods.length < 100
116 case @columns
116 case @columns
117 when 'year'
117 when 'year'
118 @periods << "#{date_from.year}"
118 @periods << "#{date_from.year}"
119 date_from = (date_from + 1.year).at_beginning_of_year
119 date_from = (date_from + 1.year).at_beginning_of_year
120 when 'month'
120 when 'month'
121 @periods << "#{date_from.year}-#{date_from.month}"
121 @periods << "#{date_from.year}-#{date_from.month}"
122 date_from = (date_from + 1.month).at_beginning_of_month
122 date_from = (date_from + 1.month).at_beginning_of_month
123 when 'week'
123 when 'week'
124 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
124 @periods << "#{date_from.year}-#{date_from.to_date.cweek}"
125 date_from = (date_from + 7.day).at_beginning_of_week
125 date_from = (date_from + 7.day).at_beginning_of_week
126 when 'day'
126 when 'day'
127 @periods << "#{date_from.to_date}"
127 @periods << "#{date_from.to_date}"
128 date_from = date_from + 1.day
128 date_from = date_from + 1.day
129 end
129 end
130 end
130 end
131 end
131 end
132
132
133 respond_to do |format|
133 respond_to do |format|
134 format.html { render :layout => !request.xhr? }
134 format.html { render :layout => !request.xhr? }
135 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
135 format.csv { send_data(report_to_csv(@criterias, @periods, @hours).read, :type => 'text/csv; header=present', :filename => 'timelog.csv') }
136 end
136 end
137 end
137 end
138
138
139 def details
139 def details
140 sort_init 'spent_on', 'desc'
140 sort_init 'spent_on', 'desc'
141 sort_update
141 sort_update 'spent_on' => 'spent_on',
142 'user' => 'user_id',
143 'activity' => 'activity_id',
144 'project' => "#{Project.table_name}.name",
145 'issue' => 'issue_id',
146 'hours' => 'hours'
142
147
143 cond = ARCondition.new
148 cond = ARCondition.new
144 if @project.nil?
149 if @project.nil?
145 cond << Project.allowed_to_condition(User.current, :view_time_entries)
150 cond << Project.allowed_to_condition(User.current, :view_time_entries)
146 elsif @issue.nil?
151 elsif @issue.nil?
147 cond << @project.project_condition(Setting.display_subprojects_issues?)
152 cond << @project.project_condition(Setting.display_subprojects_issues?)
148 else
153 else
149 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
154 cond << ["#{TimeEntry.table_name}.issue_id = ?", @issue.id]
150 end
155 end
151
156
152 retrieve_date_range
157 retrieve_date_range
153 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
158 cond << ['spent_on BETWEEN ? AND ?', @from, @to]
154
159
155 TimeEntry.visible_by(User.current) do
160 TimeEntry.visible_by(User.current) do
156 respond_to do |format|
161 respond_to do |format|
157 format.html {
162 format.html {
158 # Paginate results
163 # Paginate results
159 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
164 @entry_count = TimeEntry.count(:include => :project, :conditions => cond.conditions)
160 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
165 @entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
161 @entries = TimeEntry.find(:all,
166 @entries = TimeEntry.find(:all,
162 :include => [:project, :activity, :user, {:issue => :tracker}],
167 :include => [:project, :activity, :user, {:issue => :tracker}],
163 :conditions => cond.conditions,
168 :conditions => cond.conditions,
164 :order => sort_clause,
169 :order => sort_clause,
165 :limit => @entry_pages.items_per_page,
170 :limit => @entry_pages.items_per_page,
166 :offset => @entry_pages.current.offset)
171 :offset => @entry_pages.current.offset)
167 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
172 @total_hours = TimeEntry.sum(:hours, :include => :project, :conditions => cond.conditions).to_f
168
173
169 render :layout => !request.xhr?
174 render :layout => !request.xhr?
170 }
175 }
171 format.atom {
176 format.atom {
172 entries = TimeEntry.find(:all,
177 entries = TimeEntry.find(:all,
173 :include => [:project, :activity, :user, {:issue => :tracker}],
178 :include => [:project, :activity, :user, {:issue => :tracker}],
174 :conditions => cond.conditions,
179 :conditions => cond.conditions,
175 :order => "#{TimeEntry.table_name}.created_on DESC",
180 :order => "#{TimeEntry.table_name}.created_on DESC",
176 :limit => Setting.feeds_limit.to_i)
181 :limit => Setting.feeds_limit.to_i)
177 render_feed(entries, :title => l(:label_spent_time))
182 render_feed(entries, :title => l(:label_spent_time))
178 }
183 }
179 format.csv {
184 format.csv {
180 # Export all entries
185 # Export all entries
181 @entries = TimeEntry.find(:all,
186 @entries = TimeEntry.find(:all,
182 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
187 :include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
183 :conditions => cond.conditions,
188 :conditions => cond.conditions,
184 :order => sort_clause)
189 :order => sort_clause)
185 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
190 send_data(entries_to_csv(@entries).read, :type => 'text/csv; header=present', :filename => 'timelog.csv')
186 }
191 }
187 end
192 end
188 end
193 end
189 end
194 end
190
195
191 def edit
196 def edit
192 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
197 render_403 and return if @time_entry && !@time_entry.editable_by?(User.current)
193 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
198 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
194 @time_entry.attributes = params[:time_entry]
199 @time_entry.attributes = params[:time_entry]
195 if request.post? and @time_entry.save
200 if request.post? and @time_entry.save
196 flash[:notice] = l(:notice_successful_update)
201 flash[:notice] = l(:notice_successful_update)
197 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
202 redirect_back_or_default :action => 'details', :project_id => @time_entry.project
198 return
203 return
199 end
204 end
200 end
205 end
201
206
202 def destroy
207 def destroy
203 render_404 and return unless @time_entry
208 render_404 and return unless @time_entry
204 render_403 and return unless @time_entry.editable_by?(User.current)
209 render_403 and return unless @time_entry.editable_by?(User.current)
205 @time_entry.destroy
210 @time_entry.destroy
206 flash[:notice] = l(:notice_successful_delete)
211 flash[:notice] = l(:notice_successful_delete)
207 redirect_to :back
212 redirect_to :back
208 rescue ::ActionController::RedirectBackError
213 rescue ::ActionController::RedirectBackError
209 redirect_to :action => 'details', :project_id => @time_entry.project
214 redirect_to :action => 'details', :project_id => @time_entry.project
210 end
215 end
211
216
212 private
217 private
213 def find_project
218 def find_project
214 if params[:id]
219 if params[:id]
215 @time_entry = TimeEntry.find(params[:id])
220 @time_entry = TimeEntry.find(params[:id])
216 @project = @time_entry.project
221 @project = @time_entry.project
217 elsif params[:issue_id]
222 elsif params[:issue_id]
218 @issue = Issue.find(params[:issue_id])
223 @issue = Issue.find(params[:issue_id])
219 @project = @issue.project
224 @project = @issue.project
220 elsif params[:project_id]
225 elsif params[:project_id]
221 @project = Project.find(params[:project_id])
226 @project = Project.find(params[:project_id])
222 else
227 else
223 render_404
228 render_404
224 return false
229 return false
225 end
230 end
226 rescue ActiveRecord::RecordNotFound
231 rescue ActiveRecord::RecordNotFound
227 render_404
232 render_404
228 end
233 end
229
234
230 def find_optional_project
235 def find_optional_project
231 if !params[:issue_id].blank?
236 if !params[:issue_id].blank?
232 @issue = Issue.find(params[:issue_id])
237 @issue = Issue.find(params[:issue_id])
233 @project = @issue.project
238 @project = @issue.project
234 elsif !params[:project_id].blank?
239 elsif !params[:project_id].blank?
235 @project = Project.find(params[:project_id])
240 @project = Project.find(params[:project_id])
236 end
241 end
237 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
242 deny_access unless User.current.allowed_to?(:view_time_entries, @project, :global => true)
238 end
243 end
239
244
240 # Retrieves the date range based on predefined ranges or specific from/to param dates
245 # Retrieves the date range based on predefined ranges or specific from/to param dates
241 def retrieve_date_range
246 def retrieve_date_range
242 @free_period = false
247 @free_period = false
243 @from, @to = nil, nil
248 @from, @to = nil, nil
244
249
245 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
250 if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
246 case params[:period].to_s
251 case params[:period].to_s
247 when 'today'
252 when 'today'
248 @from = @to = Date.today
253 @from = @to = Date.today
249 when 'yesterday'
254 when 'yesterday'
250 @from = @to = Date.today - 1
255 @from = @to = Date.today - 1
251 when 'current_week'
256 when 'current_week'
252 @from = Date.today - (Date.today.cwday - 1)%7
257 @from = Date.today - (Date.today.cwday - 1)%7
253 @to = @from + 6
258 @to = @from + 6
254 when 'last_week'
259 when 'last_week'
255 @from = Date.today - 7 - (Date.today.cwday - 1)%7
260 @from = Date.today - 7 - (Date.today.cwday - 1)%7
256 @to = @from + 6
261 @to = @from + 6
257 when '7_days'
262 when '7_days'
258 @from = Date.today - 7
263 @from = Date.today - 7
259 @to = Date.today
264 @to = Date.today
260 when 'current_month'
265 when 'current_month'
261 @from = Date.civil(Date.today.year, Date.today.month, 1)
266 @from = Date.civil(Date.today.year, Date.today.month, 1)
262 @to = (@from >> 1) - 1
267 @to = (@from >> 1) - 1
263 when 'last_month'
268 when 'last_month'
264 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
269 @from = Date.civil(Date.today.year, Date.today.month, 1) << 1
265 @to = (@from >> 1) - 1
270 @to = (@from >> 1) - 1
266 when '30_days'
271 when '30_days'
267 @from = Date.today - 30
272 @from = Date.today - 30
268 @to = Date.today
273 @to = Date.today
269 when 'current_year'
274 when 'current_year'
270 @from = Date.civil(Date.today.year, 1, 1)
275 @from = Date.civil(Date.today.year, 1, 1)
271 @to = Date.civil(Date.today.year, 12, 31)
276 @to = Date.civil(Date.today.year, 12, 31)
272 end
277 end
273 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
278 elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
274 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
279 begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
275 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
280 begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
276 @free_period = true
281 @free_period = true
277 else
282 else
278 # default
283 # default
279 end
284 end
280
285
281 @from, @to = @to, @from if @from && @to && @from > @to
286 @from, @to = @to, @from if @from && @to && @from > @to
282 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
287 @from ||= (TimeEntry.minimum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today) - 1
283 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
288 @to ||= (TimeEntry.maximum(:spent_on, :include => :project, :conditions => Project.allowed_to_condition(User.current, :view_time_entries)) || Date.today)
284 end
289 end
285 end
290 end
@@ -1,104 +1,104
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class UsersController < ApplicationController
18 class UsersController < ApplicationController
19 before_filter :require_admin
19 before_filter :require_admin
20
20
21 helper :sort
21 helper :sort
22 include SortHelper
22 include SortHelper
23 helper :custom_fields
23 helper :custom_fields
24 include CustomFieldsHelper
24 include CustomFieldsHelper
25
25
26 def index
26 def index
27 list
27 list
28 render :action => 'list' unless request.xhr?
28 render :action => 'list' unless request.xhr?
29 end
29 end
30
30
31 def list
31 def list
32 sort_init 'login', 'asc'
32 sort_init 'login', 'asc'
33 sort_update
33 sort_update %w(login firstname lastname mail admin created_on last_login_on)
34
34
35 @status = params[:status] ? params[:status].to_i : 1
35 @status = params[:status] ? params[:status].to_i : 1
36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
36 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
37
37
38 unless params[:name].blank?
38 unless params[:name].blank?
39 name = "%#{params[:name].strip.downcase}%"
39 name = "%#{params[:name].strip.downcase}%"
40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
40 c << ["LOWER(login) LIKE ? OR LOWER(firstname) LIKE ? OR LOWER(lastname) LIKE ?", name, name, name]
41 end
41 end
42
42
43 @user_count = User.count(:conditions => c.conditions)
43 @user_count = User.count(:conditions => c.conditions)
44 @user_pages = Paginator.new self, @user_count,
44 @user_pages = Paginator.new self, @user_count,
45 per_page_option,
45 per_page_option,
46 params['page']
46 params['page']
47 @users = User.find :all,:order => sort_clause,
47 @users = User.find :all,:order => sort_clause,
48 :conditions => c.conditions,
48 :conditions => c.conditions,
49 :limit => @user_pages.items_per_page,
49 :limit => @user_pages.items_per_page,
50 :offset => @user_pages.current.offset
50 :offset => @user_pages.current.offset
51
51
52 render :action => "list", :layout => false if request.xhr?
52 render :action => "list", :layout => false if request.xhr?
53 end
53 end
54
54
55 def add
55 def add
56 if request.get?
56 if request.get?
57 @user = User.new(:language => Setting.default_language)
57 @user = User.new(:language => Setting.default_language)
58 else
58 else
59 @user = User.new(params[:user])
59 @user = User.new(params[:user])
60 @user.admin = params[:user][:admin] || false
60 @user.admin = params[:user][:admin] || false
61 @user.login = params[:user][:login]
61 @user.login = params[:user][:login]
62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
62 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless @user.auth_source_id
63 if @user.save
63 if @user.save
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
64 Mailer.deliver_account_information(@user, params[:password]) if params[:send_information]
65 flash[:notice] = l(:notice_successful_create)
65 flash[:notice] = l(:notice_successful_create)
66 redirect_to :action => 'list'
66 redirect_to :action => 'list'
67 end
67 end
68 end
68 end
69 @auth_sources = AuthSource.find(:all)
69 @auth_sources = AuthSource.find(:all)
70 end
70 end
71
71
72 def edit
72 def edit
73 @user = User.find(params[:id])
73 @user = User.find(params[:id])
74 if request.post?
74 if request.post?
75 @user.admin = params[:user][:admin] if params[:user][:admin]
75 @user.admin = params[:user][:admin] if params[:user][:admin]
76 @user.login = params[:user][:login] if params[:user][:login]
76 @user.login = params[:user][:login] if params[:user][:login]
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
77 @user.password, @user.password_confirmation = params[:password], params[:password_confirmation] unless params[:password].nil? or params[:password].empty? or @user.auth_source_id
78 if @user.update_attributes(params[:user])
78 if @user.update_attributes(params[:user])
79 flash[:notice] = l(:notice_successful_update)
79 flash[:notice] = l(:notice_successful_update)
80 # Give a string to redirect_to otherwise it would use status param as the response code
80 # Give a string to redirect_to otherwise it would use status param as the response code
81 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
81 redirect_to(url_for(:action => 'list', :status => params[:status], :page => params[:page]))
82 end
82 end
83 end
83 end
84 @auth_sources = AuthSource.find(:all)
84 @auth_sources = AuthSource.find(:all)
85 @roles = Role.find_all_givable
85 @roles = Role.find_all_givable
86 @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
86 @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects
87 @membership ||= Member.new
87 @membership ||= Member.new
88 @memberships = @user.memberships
88 @memberships = @user.memberships
89 end
89 end
90
90
91 def edit_membership
91 def edit_membership
92 @user = User.find(params[:id])
92 @user = User.find(params[:id])
93 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
93 @membership = params[:membership_id] ? Member.find(params[:membership_id]) : Member.new(:user => @user)
94 @membership.attributes = params[:membership]
94 @membership.attributes = params[:membership]
95 @membership.save if request.post?
95 @membership.save if request.post?
96 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
96 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
97 end
97 end
98
98
99 def destroy_membership
99 def destroy_membership
100 @user = User.find(params[:id])
100 @user = User.find(params[:id])
101 Member.find(params[:membership_id]).destroy if request.post?
101 Member.find(params[:membership_id]).destroy if request.post?
102 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
102 redirect_to :action => 'edit', :id => @user, :tab => 'memberships'
103 end
103 end
104 end
104 end
@@ -1,55 +1,55
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module QueriesHelper
18 module QueriesHelper
19
19
20 def operators_for_select(filter_type)
20 def operators_for_select(filter_type)
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 end
22 end
23
23
24 def column_header(column)
24 def column_header(column)
25 column.sortable ? sort_header_tag(column.sortable, :caption => column.caption,
25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 :default_order => column.default_order) :
26 :default_order => column.default_order) :
27 content_tag('th', column.caption)
27 content_tag('th', column.caption)
28 end
28 end
29
29
30 def column_content(column, issue)
30 def column_content(column, issue)
31 if column.is_a?(QueryCustomFieldColumn)
31 if column.is_a?(QueryCustomFieldColumn)
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 show_value(cv)
33 show_value(cv)
34 else
34 else
35 value = issue.send(column.name)
35 value = issue.send(column.name)
36 if value.is_a?(Date)
36 if value.is_a?(Date)
37 format_date(value)
37 format_date(value)
38 elsif value.is_a?(Time)
38 elsif value.is_a?(Time)
39 format_time(value)
39 format_time(value)
40 else
40 else
41 case column.name
41 case column.name
42 when :subject
42 when :subject
43 h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
43 h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
44 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
44 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
45 when :done_ratio
45 when :done_ratio
46 progress_bar(value, :width => '80px')
46 progress_bar(value, :width => '80px')
47 when :fixed_version
47 when :fixed_version
48 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
48 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
49 else
49 else
50 h(value)
50 h(value)
51 end
51 end
52 end
52 end
53 end
53 end
54 end
54 end
55 end
55 end
@@ -1,160 +1,168
1 # Helpers to sort tables using clickable column headers.
1 # Helpers to sort tables using clickable column headers.
2 #
2 #
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
3 # Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
4 # License: This source code is released under the MIT license.
4 # License: This source code is released under the MIT license.
5 #
5 #
6 # - Consecutive clicks toggle the column's sort order.
6 # - Consecutive clicks toggle the column's sort order.
7 # - Sort state is maintained by a session hash entry.
7 # - Sort state is maintained by a session hash entry.
8 # - Icon image identifies sort column and state.
8 # - Icon image identifies sort column and state.
9 # - Typically used in conjunction with the Pagination module.
9 # - Typically used in conjunction with the Pagination module.
10 #
10 #
11 # Example code snippets:
11 # Example code snippets:
12 #
12 #
13 # Controller:
13 # Controller:
14 #
14 #
15 # helper :sort
15 # helper :sort
16 # include SortHelper
16 # include SortHelper
17 #
17 #
18 # def list
18 # def list
19 # sort_init 'last_name'
19 # sort_init 'last_name'
20 # sort_update
20 # sort_update
21 # @items = Contact.find_all nil, sort_clause
21 # @items = Contact.find_all nil, sort_clause
22 # end
22 # end
23 #
23 #
24 # Controller (using Pagination module):
24 # Controller (using Pagination module):
25 #
25 #
26 # helper :sort
26 # helper :sort
27 # include SortHelper
27 # include SortHelper
28 #
28 #
29 # def list
29 # def list
30 # sort_init 'last_name'
30 # sort_init 'last_name'
31 # sort_update
31 # sort_update
32 # @contact_pages, @items = paginate :contacts,
32 # @contact_pages, @items = paginate :contacts,
33 # :order_by => sort_clause,
33 # :order_by => sort_clause,
34 # :per_page => 10
34 # :per_page => 10
35 # end
35 # end
36 #
36 #
37 # View (table header in list.rhtml):
37 # View (table header in list.rhtml):
38 #
38 #
39 # <thead>
39 # <thead>
40 # <tr>
40 # <tr>
41 # <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
41 # <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
42 # <%= sort_header_tag('last_name', :caption => 'Name') %>
42 # <%= sort_header_tag('last_name', :caption => 'Name') %>
43 # <%= sort_header_tag('phone') %>
43 # <%= sort_header_tag('phone') %>
44 # <%= sort_header_tag('address', :width => 200) %>
44 # <%= sort_header_tag('address', :width => 200) %>
45 # </tr>
45 # </tr>
46 # </thead>
46 # </thead>
47 #
47 #
48 # - The ascending and descending sort icon images are sort_asc.png and
48 # - The ascending and descending sort icon images are sort_asc.png and
49 # sort_desc.png and reside in the application's images directory.
49 # sort_desc.png and reside in the application's images directory.
50 # - Introduces instance variables: @sort_name, @sort_default.
50 # - Introduces instance variables: @sort_name, @sort_default.
51 # - Introduces params :sort_key and :sort_order.
51 # - Introduces params :sort_key and :sort_order.
52 #
52 #
53 module SortHelper
53 module SortHelper
54
54
55 # Initializes the default sort column (default_key) and sort order
55 # Initializes the default sort column (default_key) and sort order
56 # (default_order).
56 # (default_order).
57 #
57 #
58 # - default_key is a column attribute name.
58 # - default_key is a column attribute name.
59 # - default_order is 'asc' or 'desc'.
59 # - default_order is 'asc' or 'desc'.
60 # - name is the name of the session hash entry that stores the sort state,
60 # - name is the name of the session hash entry that stores the sort state,
61 # defaults to '<controller_name>_sort'.
61 # defaults to '<controller_name>_sort'.
62 #
62 #
63 def sort_init(default_key, default_order='asc', name=nil)
63 def sort_init(default_key, default_order='asc', name=nil)
64 @sort_name = name || params[:controller] + params[:action] + '_sort'
64 @sort_name = name || params[:controller] + params[:action] + '_sort'
65 @sort_default = {:key => default_key, :order => default_order}
65 @sort_default = {:key => default_key, :order => default_order}
66 end
66 end
67
67
68 # Updates the sort state. Call this in the controller prior to calling
68 # Updates the sort state. Call this in the controller prior to calling
69 # sort_clause.
69 # sort_clause.
70 #
70 # sort_keys can be either an array or a hash of allowed keys
71 def sort_update()
71 def sort_update(sort_keys)
72 if params[:sort_key]
72 sort_key = params[:sort_key]
73 sort = {:key => params[:sort_key], :order => params[:sort_order]}
73 sort_key = nil unless (sort_keys.is_a?(Array) ? sort_keys.include?(sort_key) : sort_keys[sort_key])
74
75 sort_order = (params[:sort_order] == 'desc' ? 'DESC' : 'ASC')
76
77 if sort_key
78 sort = {:key => sort_key, :order => sort_order}
74 elsif session[@sort_name]
79 elsif session[@sort_name]
75 sort = session[@sort_name] # Previous sort.
80 sort = session[@sort_name] # Previous sort.
76 else
81 else
77 sort = @sort_default
82 sort = @sort_default
78 end
83 end
79 session[@sort_name] = sort
84 session[@sort_name] = sort
85
86 sort_column = (sort_keys.is_a?(Hash) ? sort_keys[sort[:key]] : sort[:key])
87 @sort_clause = (sort_column.blank? ? '' : "#{sort_column} #{sort[:order]}")
80 end
88 end
81
89
82 # Returns an SQL sort clause corresponding to the current sort state.
90 # Returns an SQL sort clause corresponding to the current sort state.
83 # Use this to sort the controller's table items collection.
91 # Use this to sort the controller's table items collection.
84 #
92 #
85 def sort_clause()
93 def sort_clause()
86 session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
94 @sort_clause || '' #session[@sort_name][:key] + ' ' + (session[@sort_name][:order] || 'ASC')
87 end
95 end
88
96
89 # Returns a link which sorts by the named column.
97 # Returns a link which sorts by the named column.
90 #
98 #
91 # - column is the name of an attribute in the sorted record collection.
99 # - column is the name of an attribute in the sorted record collection.
92 # - The optional caption explicitly specifies the displayed link text.
100 # - The optional caption explicitly specifies the displayed link text.
93 # - A sort icon image is positioned to the right of the sort link.
101 # - A sort icon image is positioned to the right of the sort link.
94 #
102 #
95 def sort_link(column, caption, default_order)
103 def sort_link(column, caption, default_order)
96 key, order = session[@sort_name][:key], session[@sort_name][:order]
104 key, order = session[@sort_name][:key], session[@sort_name][:order]
97 if key == column
105 if key == column
98 if order.downcase == 'asc'
106 if order.downcase == 'asc'
99 icon = 'sort_asc.png'
107 icon = 'sort_asc.png'
100 order = 'desc'
108 order = 'desc'
101 else
109 else
102 icon = 'sort_desc.png'
110 icon = 'sort_desc.png'
103 order = 'asc'
111 order = 'asc'
104 end
112 end
105 else
113 else
106 icon = nil
114 icon = nil
107 order = default_order
115 order = default_order
108 end
116 end
109 caption = titleize(Inflector::humanize(column)) unless caption
117 caption = titleize(Inflector::humanize(column)) unless caption
110
118
111 sort_options = { :sort_key => column, :sort_order => order }
119 sort_options = { :sort_key => column, :sort_order => order }
112 # don't reuse params if filters are present
120 # don't reuse params if filters are present
113 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
121 url_options = params.has_key?(:set_filter) ? sort_options : params.merge(sort_options)
114
122
115 link_to_remote(caption,
123 link_to_remote(caption,
116 {:update => "content", :url => url_options},
124 {:update => "content", :url => url_options},
117 {:href => url_for(url_options)}) +
125 {:href => url_for(url_options)}) +
118 (icon ? nbsp(2) + image_tag(icon) : '')
126 (icon ? nbsp(2) + image_tag(icon) : '')
119 end
127 end
120
128
121 # Returns a table header <th> tag with a sort link for the named column
129 # Returns a table header <th> tag with a sort link for the named column
122 # attribute.
130 # attribute.
123 #
131 #
124 # Options:
132 # Options:
125 # :caption The displayed link name (defaults to titleized column name).
133 # :caption The displayed link name (defaults to titleized column name).
126 # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
134 # :title The tag's 'title' attribute (defaults to 'Sort by :caption').
127 #
135 #
128 # Other options hash entries generate additional table header tag attributes.
136 # Other options hash entries generate additional table header tag attributes.
129 #
137 #
130 # Example:
138 # Example:
131 #
139 #
132 # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
140 # <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
133 #
141 #
134 # Renders:
142 # Renders:
135 #
143 #
136 # <th title="Sort by contact ID" width="40">
144 # <th title="Sort by contact ID" width="40">
137 # <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
145 # <a href="/contact/list?sort_order=desc&amp;sort_key=id">Id</a>
138 # &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
146 # &nbsp;&nbsp;<img alt="Sort_asc" src="/images/sort_asc.png" />
139 # </th>
147 # </th>
140 #
148 #
141 def sort_header_tag(column, options = {})
149 def sort_header_tag(column, options = {})
142 caption = options.delete(:caption) || titleize(Inflector::humanize(column))
150 caption = options.delete(:caption) || titleize(Inflector::humanize(column))
143 default_order = options.delete(:default_order) || 'asc'
151 default_order = options.delete(:default_order) || 'asc'
144 options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
152 options[:title]= l(:label_sort_by, "\"#{caption}\"") unless options[:title]
145 content_tag('th', sort_link(column, caption, default_order), options)
153 content_tag('th', sort_link(column, caption, default_order), options)
146 end
154 end
147
155
148 private
156 private
149
157
150 # Return n non-breaking spaces.
158 # Return n non-breaking spaces.
151 def nbsp(n)
159 def nbsp(n)
152 '&nbsp;' * n
160 '&nbsp;' * n
153 end
161 end
154
162
155 # Return capitalized title.
163 # Return capitalized title.
156 def titleize(title)
164 def titleize(title)
157 title.split.map {|w| w.capitalize }.join(' ')
165 title.split.map {|w| w.capitalize }.join(' ')
158 end
166 end
159
167
160 end
168 end
@@ -1,247 +1,247
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Mailer < ActionMailer::Base
18 class Mailer < ActionMailer::Base
19 helper :application
19 helper :application
20 helper :issues
20 helper :issues
21 helper :custom_fields
21 helper :custom_fields
22
22
23 include ActionController::UrlWriter
23 include ActionController::UrlWriter
24
24
25 def issue_add(issue)
25 def issue_add(issue)
26 redmine_headers 'Project' => issue.project.identifier,
26 redmine_headers 'Project' => issue.project.identifier,
27 'Issue-Id' => issue.id,
27 'Issue-Id' => issue.id,
28 'Issue-Author' => issue.author.login
28 'Issue-Author' => issue.author.login
29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
29 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
30 recipients issue.recipients
30 recipients issue.recipients
31 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
31 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
32 body :issue => issue,
32 body :issue => issue,
33 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
33 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
34 end
34 end
35
35
36 def issue_edit(journal)
36 def issue_edit(journal)
37 issue = journal.journalized
37 issue = journal.journalized
38 redmine_headers 'Project' => issue.project.identifier,
38 redmine_headers 'Project' => issue.project.identifier,
39 'Issue-Id' => issue.id,
39 'Issue-Id' => issue.id,
40 'Issue-Author' => issue.author.login
40 'Issue-Author' => issue.author.login
41 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
41 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
42 recipients issue.recipients
42 recipients issue.recipients
43 # Watchers in cc
43 # Watchers in cc
44 cc(issue.watcher_recipients - @recipients)
44 cc(issue.watcher_recipients - @recipients)
45 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
45 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
46 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
46 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
47 s << issue.subject
47 s << issue.subject
48 subject s
48 subject s
49 body :issue => issue,
49 body :issue => issue,
50 :journal => journal,
50 :journal => journal,
51 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
51 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
52 end
52 end
53
53
54 def reminder(user, issues, days)
54 def reminder(user, issues, days)
55 set_language_if_valid user.language
55 set_language_if_valid user.language
56 recipients user.mail
56 recipients user.mail
57 subject l(:mail_subject_reminder, issues.size)
57 subject l(:mail_subject_reminder, issues.size)
58 body :issues => issues,
58 body :issues => issues,
59 :days => days,
59 :days => days,
60 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'issues.due_date', :sort_order => 'asc')
60 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
61 end
61 end
62
62
63 def document_added(document)
63 def document_added(document)
64 redmine_headers 'Project' => document.project.identifier
64 redmine_headers 'Project' => document.project.identifier
65 recipients document.project.recipients
65 recipients document.project.recipients
66 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
66 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
67 body :document => document,
67 body :document => document,
68 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
68 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
69 end
69 end
70
70
71 def attachments_added(attachments)
71 def attachments_added(attachments)
72 container = attachments.first.container
72 container = attachments.first.container
73 added_to = ''
73 added_to = ''
74 added_to_url = ''
74 added_to_url = ''
75 case container.class.name
75 case container.class.name
76 when 'Version'
76 when 'Version'
77 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
77 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
78 added_to = "#{l(:label_version)}: #{container.name}"
78 added_to = "#{l(:label_version)}: #{container.name}"
79 when 'Document'
79 when 'Document'
80 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
80 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
81 added_to = "#{l(:label_document)}: #{container.title}"
81 added_to = "#{l(:label_document)}: #{container.title}"
82 end
82 end
83 redmine_headers 'Project' => container.project.identifier
83 redmine_headers 'Project' => container.project.identifier
84 recipients container.project.recipients
84 recipients container.project.recipients
85 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
85 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
86 body :attachments => attachments,
86 body :attachments => attachments,
87 :added_to => added_to,
87 :added_to => added_to,
88 :added_to_url => added_to_url
88 :added_to_url => added_to_url
89 end
89 end
90
90
91 def news_added(news)
91 def news_added(news)
92 redmine_headers 'Project' => news.project.identifier
92 redmine_headers 'Project' => news.project.identifier
93 recipients news.project.recipients
93 recipients news.project.recipients
94 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
94 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
95 body :news => news,
95 body :news => news,
96 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
96 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
97 end
97 end
98
98
99 def message_posted(message, recipients)
99 def message_posted(message, recipients)
100 redmine_headers 'Project' => message.project.identifier,
100 redmine_headers 'Project' => message.project.identifier,
101 'Topic-Id' => (message.parent_id || message.id)
101 'Topic-Id' => (message.parent_id || message.id)
102 recipients(recipients)
102 recipients(recipients)
103 subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
103 subject "[#{message.board.project.name} - #{message.board.name}] #{message.subject}"
104 body :message => message,
104 body :message => message,
105 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
105 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
106 end
106 end
107
107
108 def account_information(user, password)
108 def account_information(user, password)
109 set_language_if_valid user.language
109 set_language_if_valid user.language
110 recipients user.mail
110 recipients user.mail
111 subject l(:mail_subject_register, Setting.app_title)
111 subject l(:mail_subject_register, Setting.app_title)
112 body :user => user,
112 body :user => user,
113 :password => password,
113 :password => password,
114 :login_url => url_for(:controller => 'account', :action => 'login')
114 :login_url => url_for(:controller => 'account', :action => 'login')
115 end
115 end
116
116
117 def account_activation_request(user)
117 def account_activation_request(user)
118 # Send the email to all active administrators
118 # Send the email to all active administrators
119 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
119 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
120 subject l(:mail_subject_account_activation_request, Setting.app_title)
120 subject l(:mail_subject_account_activation_request, Setting.app_title)
121 body :user => user,
121 body :user => user,
122 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
122 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
123 end
123 end
124
124
125 def lost_password(token)
125 def lost_password(token)
126 set_language_if_valid(token.user.language)
126 set_language_if_valid(token.user.language)
127 recipients token.user.mail
127 recipients token.user.mail
128 subject l(:mail_subject_lost_password, Setting.app_title)
128 subject l(:mail_subject_lost_password, Setting.app_title)
129 body :token => token,
129 body :token => token,
130 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
130 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
131 end
131 end
132
132
133 def register(token)
133 def register(token)
134 set_language_if_valid(token.user.language)
134 set_language_if_valid(token.user.language)
135 recipients token.user.mail
135 recipients token.user.mail
136 subject l(:mail_subject_register, Setting.app_title)
136 subject l(:mail_subject_register, Setting.app_title)
137 body :token => token,
137 body :token => token,
138 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
138 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
139 end
139 end
140
140
141 def test(user)
141 def test(user)
142 set_language_if_valid(user.language)
142 set_language_if_valid(user.language)
143 recipients user.mail
143 recipients user.mail
144 subject 'Redmine test'
144 subject 'Redmine test'
145 body :url => url_for(:controller => 'welcome')
145 body :url => url_for(:controller => 'welcome')
146 end
146 end
147
147
148 # Overrides default deliver! method to prevent from sending an email
148 # Overrides default deliver! method to prevent from sending an email
149 # with no recipient, cc or bcc
149 # with no recipient, cc or bcc
150 def deliver!(mail = @mail)
150 def deliver!(mail = @mail)
151 return false if (recipients.nil? || recipients.empty?) &&
151 return false if (recipients.nil? || recipients.empty?) &&
152 (cc.nil? || cc.empty?) &&
152 (cc.nil? || cc.empty?) &&
153 (bcc.nil? || bcc.empty?)
153 (bcc.nil? || bcc.empty?)
154 super
154 super
155 end
155 end
156
156
157 # Sends reminders to issue assignees
157 # Sends reminders to issue assignees
158 # Available options:
158 # Available options:
159 # * :days => how many days in the future to remind about (defaults to 7)
159 # * :days => how many days in the future to remind about (defaults to 7)
160 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
160 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
161 # * :project => id or identifier of project to process (defaults to all projects)
161 # * :project => id or identifier of project to process (defaults to all projects)
162 def self.reminders(options={})
162 def self.reminders(options={})
163 days = options[:days] || 7
163 days = options[:days] || 7
164 project = options[:project] ? Project.find(options[:project]) : nil
164 project = options[:project] ? Project.find(options[:project]) : nil
165 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
165 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
166
166
167 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
167 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
168 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
168 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
169 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
169 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
170 s << "#{Issue.table_name}.project_id = #{project.id}" if project
170 s << "#{Issue.table_name}.project_id = #{project.id}" if project
171 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
171 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
172
172
173 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
173 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
174 :conditions => s.conditions
174 :conditions => s.conditions
175 ).group_by(&:assigned_to)
175 ).group_by(&:assigned_to)
176 issues_by_assignee.each do |assignee, issues|
176 issues_by_assignee.each do |assignee, issues|
177 deliver_reminder(assignee, issues, days) unless assignee.nil?
177 deliver_reminder(assignee, issues, days) unless assignee.nil?
178 end
178 end
179 end
179 end
180
180
181 private
181 private
182 def initialize_defaults(method_name)
182 def initialize_defaults(method_name)
183 super
183 super
184 set_language_if_valid Setting.default_language
184 set_language_if_valid Setting.default_language
185 from Setting.mail_from
185 from Setting.mail_from
186
186
187 # URL options
187 # URL options
188 h = Setting.host_name
188 h = Setting.host_name
189 h = h.to_s.gsub(%r{\/.*$}, '') unless ActionController::AbstractRequest.relative_url_root.blank?
189 h = h.to_s.gsub(%r{\/.*$}, '') unless ActionController::AbstractRequest.relative_url_root.blank?
190 default_url_options[:host] = h
190 default_url_options[:host] = h
191 default_url_options[:protocol] = Setting.protocol
191 default_url_options[:protocol] = Setting.protocol
192
192
193 # Common headers
193 # Common headers
194 headers 'X-Mailer' => 'Redmine',
194 headers 'X-Mailer' => 'Redmine',
195 'X-Redmine-Host' => Setting.host_name,
195 'X-Redmine-Host' => Setting.host_name,
196 'X-Redmine-Site' => Setting.app_title
196 'X-Redmine-Site' => Setting.app_title
197 end
197 end
198
198
199 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
199 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
200 def redmine_headers(h)
200 def redmine_headers(h)
201 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
201 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
202 end
202 end
203
203
204 # Overrides the create_mail method
204 # Overrides the create_mail method
205 def create_mail
205 def create_mail
206 # Removes the current user from the recipients and cc
206 # Removes the current user from the recipients and cc
207 # if he doesn't want to receive notifications about what he does
207 # if he doesn't want to receive notifications about what he does
208 if User.current.pref[:no_self_notified]
208 if User.current.pref[:no_self_notified]
209 recipients.delete(User.current.mail) if recipients
209 recipients.delete(User.current.mail) if recipients
210 cc.delete(User.current.mail) if cc
210 cc.delete(User.current.mail) if cc
211 end
211 end
212 # Blind carbon copy recipients
212 # Blind carbon copy recipients
213 if Setting.bcc_recipients?
213 if Setting.bcc_recipients?
214 bcc([recipients, cc].flatten.compact.uniq)
214 bcc([recipients, cc].flatten.compact.uniq)
215 recipients []
215 recipients []
216 cc []
216 cc []
217 end
217 end
218 super
218 super
219 end
219 end
220
220
221 # Renders a message with the corresponding layout
221 # Renders a message with the corresponding layout
222 def render_message(method_name, body)
222 def render_message(method_name, body)
223 layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
223 layout = method_name.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
224 body[:content_for_layout] = render(:file => method_name, :body => body)
224 body[:content_for_layout] = render(:file => method_name, :body => body)
225 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
225 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
226 end
226 end
227
227
228 # for the case of plain text only
228 # for the case of plain text only
229 def body(*params)
229 def body(*params)
230 value = super(*params)
230 value = super(*params)
231 if Setting.plain_text_mail?
231 if Setting.plain_text_mail?
232 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
232 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
233 unless String === @body or templates.empty?
233 unless String === @body or templates.empty?
234 template = File.basename(templates.first)
234 template = File.basename(templates.first)
235 @body[:content_for_layout] = render(:file => template, :body => @body)
235 @body[:content_for_layout] = render(:file => template, :body => @body)
236 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
236 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
237 return @body
237 return @body
238 end
238 end
239 end
239 end
240 return value
240 return value
241 end
241 end
242
242
243 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
243 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
244 def self.controller_path
244 def self.controller_path
245 ''
245 ''
246 end unless respond_to?('controller_path')
246 end unless respond_to?('controller_path')
247 end
247 end
@@ -1,62 +1,62
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %>
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}) %>
2
2
3 <div class="contextual">
3 <div class="contextual">
4 <%= link_to_if_authorized l(:label_message_new),
4 <%= link_to_if_authorized l(:label_message_new),
5 {:controller => 'messages', :action => 'new', :board_id => @board},
5 {:controller => 'messages', :action => 'new', :board_id => @board},
6 :class => 'icon icon-add',
6 :class => 'icon icon-add',
7 :onclick => 'Element.show("add-message"); return false;' %>
7 :onclick => 'Element.show("add-message"); return false;' %>
8 <%= watcher_tag(@board, User.current) %>
8 <%= watcher_tag(@board, User.current) %>
9 </div>
9 </div>
10
10
11 <div id="add-message" style="display:none;">
11 <div id="add-message" style="display:none;">
12 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
12 <h2><%= link_to h(@board.name), :controller => 'boards', :action => 'show', :project_id => @project, :id => @board %> &#187; <%= l(:label_message_new) %></h2>
13 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
13 <% form_for :message, @message, :url => {:controller => 'messages', :action => 'new', :board_id => @board}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
14 <%= render :partial => 'messages/form', :locals => {:f => f} %>
14 <%= render :partial => 'messages/form', :locals => {:f => f} %>
15 <p><%= submit_tag l(:button_create) %>
15 <p><%= submit_tag l(:button_create) %>
16 <%= link_to_remote l(:label_preview),
16 <%= link_to_remote l(:label_preview),
17 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
17 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
18 :method => 'post',
18 :method => 'post',
19 :update => 'preview',
19 :update => 'preview',
20 :with => "Form.serialize('message-form')",
20 :with => "Form.serialize('message-form')",
21 :complete => "Element.scrollTo('preview')"
21 :complete => "Element.scrollTo('preview')"
22 }, :accesskey => accesskey(:preview) %> |
22 }, :accesskey => accesskey(:preview) %> |
23 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
23 <%= link_to l(:button_cancel), "#", :onclick => 'Element.hide("add-message")' %></p>
24 <% end %>
24 <% end %>
25 <div id="preview" class="wiki"></div>
25 <div id="preview" class="wiki"></div>
26 </div>
26 </div>
27
27
28 <h2><%=h @board.name %></h2>
28 <h2><%=h @board.name %></h2>
29 <p class="subtitle"><%=h @board.description %></p>
29 <p class="subtitle"><%=h @board.description %></p>
30
30
31 <% if @topics.any? %>
31 <% if @topics.any? %>
32 <table class="list messages">
32 <table class="list messages">
33 <thead><tr>
33 <thead><tr>
34 <th><%= l(:field_subject) %></th>
34 <th><%= l(:field_subject) %></th>
35 <th><%= l(:field_author) %></th>
35 <th><%= l(:field_author) %></th>
36 <%= sort_header_tag("#{Message.table_name}.created_on", :caption => l(:field_created_on)) %>
36 <%= sort_header_tag('created_on', :caption => l(:field_created_on)) %>
37 <%= sort_header_tag("#{Message.table_name}.replies_count", :caption => l(:label_reply_plural)) %>
37 <%= sort_header_tag('replies', :caption => l(:label_reply_plural)) %>
38 <%= sort_header_tag("#{Message.table_name}.updated_on", :caption => l(:label_message_last)) %>
38 <%= sort_header_tag('updated_on', :caption => l(:label_message_last)) %>
39 </tr></thead>
39 </tr></thead>
40 <tbody>
40 <tbody>
41 <% @topics.each do |topic| %>
41 <% @topics.each do |topic| %>
42 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
42 <tr class="message <%= cycle 'odd', 'even' %> <%= topic.sticky? ? 'sticky' : '' %> <%= topic.locked? ? 'locked' : '' %>">
43 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td>
43 <td class="subject"><%= link_to h(topic.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => topic }, :class => 'icon' %></td>
44 <td class="author" align="center"><%= topic.author %></td>
44 <td class="author" align="center"><%= topic.author %></td>
45 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
45 <td class="created_on" align="center"><%= format_time(topic.created_on) %></td>
46 <td class="replies" align="center"><%= topic.replies_count %></td>
46 <td class="replies" align="center"><%= topic.replies_count %></td>
47 <td class="last_message">
47 <td class="last_message">
48 <% if topic.last_reply %>
48 <% if topic.last_reply %>
49 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
49 <%= authoring topic.last_reply.created_on, topic.last_reply.author %><br />
50 <%= link_to_message topic.last_reply %>
50 <%= link_to_message topic.last_reply %>
51 <% end %>
51 <% end %>
52 </td>
52 </td>
53 </tr>
53 </tr>
54 <% end %>
54 <% end %>
55 </tbody>
55 </tbody>
56 </table>
56 </table>
57 <p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
57 <p class="pagination"><%= pagination_links_full @topic_pages, @topic_count %></p>
58 <% else %>
58 <% else %>
59 <p class="nodata"><%= l(:label_no_data) %></p>
59 <p class="nodata"><%= l(:label_no_data) %></p>
60 <% end %>
60 <% end %>
61
61
62 <% html_title h(@board.name) %>
62 <% html_title h(@board.name) %>
@@ -1,22 +1,22
1 <% form_tag({}) do -%>
1 <% form_tag({}) do -%>
2 <table class="list issues">
2 <table class="list issues">
3 <thead><tr>
3 <thead><tr>
4 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
4 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
5 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
5 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
6 </th>
6 </th>
7 <%= sort_header_tag("#{Issue.table_name}.id", :caption => '#', :default_order => 'desc') %>
7 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
8 <% query.columns.each do |column| %>
8 <% query.columns.each do |column| %>
9 <%= column_header(column) %>
9 <%= column_header(column) %>
10 <% end %>
10 <% end %>
11 </tr></thead>
11 </tr></thead>
12 <tbody>
12 <tbody>
13 <% issues.each do |issue| -%>
13 <% issues.each do |issue| -%>
14 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>">
14 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= css_issue_classes(issue) %>">
15 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
15 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
16 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
16 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
17 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
17 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
18 </tr>
18 </tr>
19 <% end -%>
19 <% end -%>
20 </tbody>
20 </tbody>
21 </table>
21 </table>
22 <% end -%>
22 <% end -%>
@@ -1,26 +1,26
1 <h3><%= l(:label_issue_plural) %></h3>
1 <h3><%= l(:label_issue_plural) %></h3>
2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
2 <%= link_to l(:label_issue_view_all), { :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1 } %><br />
3 <% if @project %>
3 <% if @project %>
4 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
4 <%= link_to l(:field_summary), :controller => 'reports', :action => 'issue_report', :id => @project %><br />
5 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br />
5 <%= link_to l(:label_change_log), :controller => 'projects', :action => 'changelog', :id => @project %><br />
6 <% end %>
6 <% end %>
7 <%= call_hook(:view_issues_sidebar_issues_bottom) %>
7 <%= call_hook(:view_issues_sidebar_issues_bottom) %>
8
8
9 <% planning_links = []
9 <% planning_links = []
10 planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true)
10 planning_links << link_to(l(:label_calendar), :action => 'calendar', :project_id => @project) if User.current.allowed_to?(:view_calendar, @project, :global => true)
11 planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true)
11 planning_links << link_to(l(:label_gantt), :action => 'gantt', :project_id => @project) if User.current.allowed_to?(:view_gantt, @project, :global => true)
12 %>
12 %>
13 <% unless planning_links.empty? %>
13 <% unless planning_links.empty? %>
14 <h3><%= l(:label_planning) %></h3>
14 <h3><%= l(:label_planning) %></h3>
15 <p><%= planning_links.join(' | ') %></p>
15 <p><%= planning_links.join(' | ') %></p>
16 <%= call_hook(:view_issues_sidebar_planning_bottom) %>
16 <%= call_hook(:view_issues_sidebar_planning_bottom) %>
17 <% end %>
17 <% end %>
18
18
19 <% unless sidebar_queries.empty? -%>
19 <% unless sidebar_queries.empty? -%>
20 <h3><%= l(:label_query_plural) %></h3>
20 <h3><%= l(:label_query_plural) %></h3>
21
21
22 <% sidebar_queries.each do |query| -%>
22 <% sidebar_queries.each do |query| -%>
23 <%= link_to query.name, :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query %><br />
23 <%= link_to(h(query.name), :controller => 'issues', :action => 'index', :project_id => @project, :query_id => query) %><br />
24 <% end -%>
24 <% end -%>
25 <%= call_hook(:view_issues_sidebar_queries_bottom) %>
25 <%= call_hook(:view_issues_sidebar_queries_bottom) %>
26 <% end -%>
26 <% end -%>
@@ -1,44 +1,44
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
2 <%= link_to_if_authorized l(:label_attachment_new), {:controller => 'projects', :action => 'add_file', :id => @project}, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <h2><%=l(:label_attachment_plural)%></h2>
5 <h2><%=l(:label_attachment_plural)%></h2>
6
6
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
7 <% delete_allowed = authorize_for('versions', 'destroy_file') %>
8
8
9 <table class="list">
9 <table class="list">
10 <thead><tr>
10 <thead><tr>
11 <th><%=l(:field_version)%></th>
11 <th><%=l(:field_version)%></th>
12 <%= sort_header_tag("#{Attachment.table_name}.filename", :caption => l(:field_filename)) %>
12 <%= sort_header_tag('filename', :caption => l(:field_filename)) %>
13 <%= sort_header_tag("#{Attachment.table_name}.created_on", :caption => l(:label_date), :default_order => 'desc') %>
13 <%= sort_header_tag('created_on', :caption => l(:label_date), :default_order => 'desc') %>
14 <%= sort_header_tag("#{Attachment.table_name}.filesize", :caption => l(:field_filesize), :default_order => 'desc') %>
14 <%= sort_header_tag('size', :caption => l(:field_filesize), :default_order => 'desc') %>
15 <%= sort_header_tag("#{Attachment.table_name}.downloads", :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
15 <%= sort_header_tag('downloads', :caption => l(:label_downloads_abbr), :default_order => 'desc') %>
16 <th>MD5</th>
16 <th>MD5</th>
17 <% if delete_allowed %><th></th><% end %>
17 <% if delete_allowed %><th></th><% end %>
18 </tr></thead>
18 </tr></thead>
19 <tbody>
19 <tbody>
20 <% for version in @versions %>
20 <% for version in @versions %>
21 <% unless version.attachments.empty? %>
21 <% unless version.attachments.empty? %>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
22 <tr><th colspan="7" align="left"><span class="icon icon-package"><b><%= version.name %></b></span></th></tr>
23 <% for file in version.attachments %>
23 <% for file in version.attachments %>
24 <tr class="<%= cycle("odd", "even") %>">
24 <tr class="<%= cycle("odd", "even") %>">
25 <td></td>
25 <td></td>
26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
26 <td><%= link_to_attachment file, :download => true, :title => file.description %></td>
27 <td align="center"><%= format_time(file.created_on) %></td>
27 <td align="center"><%= format_time(file.created_on) %></td>
28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
28 <td align="center"><%= number_to_human_size(file.filesize) %></td>
29 <td align="center"><%= file.downloads %></td>
29 <td align="center"><%= file.downloads %></td>
30 <td align="center"><small><%= file.digest %></small></td>
30 <td align="center"><small><%= file.digest %></small></td>
31 <% if delete_allowed %>
31 <% if delete_allowed %>
32 <td align="center">
32 <td align="center">
33 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
33 <%= link_to_if_authorized image_tag('delete.png'), {:controller => 'versions', :action => 'destroy_file', :id => version, :attachment_id => file}, :confirm => l(:text_are_you_sure), :method => :post %>
34 </td>
34 </td>
35 <% end %>
35 <% end %>
36 </tr>
36 </tr>
37 <% end
37 <% end
38 reset_cycle %>
38 reset_cycle %>
39 <% end %>
39 <% end %>
40 <% end %>
40 <% end %>
41 </tbody>
41 </tbody>
42 </table>
42 </table>
43
43
44 <% html_title(l(:label_attachment_plural)) -%>
44 <% html_title(l(:label_attachment_plural)) -%>
@@ -1,41 +1,41
1 <table class="list time-entries">
1 <table class="list time-entries">
2 <thead>
2 <thead>
3 <tr>
3 <tr>
4 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
4 <%= sort_header_tag('spent_on', :caption => l(:label_date), :default_order => 'desc') %>
5 <%= sort_header_tag('user_id', :caption => l(:label_member)) %>
5 <%= sort_header_tag('user', :caption => l(:label_member)) %>
6 <%= sort_header_tag('activity_id', :caption => l(:label_activity)) %>
6 <%= sort_header_tag('activity', :caption => l(:label_activity)) %>
7 <%= sort_header_tag("#{Project.table_name}.name", :caption => l(:label_project)) %>
7 <%= sort_header_tag('project', :caption => l(:label_project)) %>
8 <%= sort_header_tag('issue_id', :caption => l(:label_issue), :default_order => 'desc') %>
8 <%= sort_header_tag('issue', :caption => l(:label_issue), :default_order => 'desc') %>
9 <th><%= l(:field_comments) %></th>
9 <th><%= l(:field_comments) %></th>
10 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
10 <%= sort_header_tag('hours', :caption => l(:field_hours)) %>
11 <th></th>
11 <th></th>
12 </tr>
12 </tr>
13 </thead>
13 </thead>
14 <tbody>
14 <tbody>
15 <% entries.each do |entry| -%>
15 <% entries.each do |entry| -%>
16 <tr class="time-entry <%= cycle("odd", "even") %>">
16 <tr class="time-entry <%= cycle("odd", "even") %>">
17 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
17 <td class="spent_on"><%= format_date(entry.spent_on) %></td>
18 <td class="user"><%=h entry.user %></td>
18 <td class="user"><%=h entry.user %></td>
19 <td class="activity"><%=h entry.activity %></td>
19 <td class="activity"><%=h entry.activity %></td>
20 <td class="project"><%=h entry.project %></td>
20 <td class="project"><%=h entry.project %></td>
21 <td class="subject">
21 <td class="subject">
22 <% if entry.issue -%>
22 <% if entry.issue -%>
23 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
23 <%= link_to_issue entry.issue %>: <%= h(truncate(entry.issue.subject, 50)) -%>
24 <% end -%>
24 <% end -%>
25 </td>
25 </td>
26 <td class="comments"><%=h entry.comments %></td>
26 <td class="comments"><%=h entry.comments %></td>
27 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
27 <td class="hours"><%= html_hours("%.2f" % entry.hours) %></td>
28 <td align="center">
28 <td align="center">
29 <% if entry.editable_by?(User.current) -%>
29 <% if entry.editable_by?(User.current) -%>
30 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
30 <%= link_to image_tag('edit.png'), {:controller => 'timelog', :action => 'edit', :id => entry, :project_id => nil},
31 :title => l(:button_edit) %>
31 :title => l(:button_edit) %>
32 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
32 <%= link_to image_tag('delete.png'), {:controller => 'timelog', :action => 'destroy', :id => entry, :project_id => nil},
33 :confirm => l(:text_are_you_sure),
33 :confirm => l(:text_are_you_sure),
34 :method => :post,
34 :method => :post,
35 :title => l(:button_delete) %>
35 :title => l(:button_delete) %>
36 <% end -%>
36 <% end -%>
37 </td>
37 </td>
38 </tr>
38 </tr>
39 <% end -%>
39 <% end -%>
40 </tbody>
40 </tbody>
41 </table>
41 </table>
@@ -1,32 +1,32
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %>
2 <%= link_to(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit') %>
3 <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
3 <%= link_to(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
4 </div>
4 </div>
5
5
6 <h2><%= @page.pretty_title %></h2>
6 <h2><%= @page.pretty_title %></h2>
7
7
8 <p>
8 <p>
9 <%= l(:label_version) %> <%= link_to @annotate.content.version, :action => 'index', :page => @page.title, :version => @annotate.content.version %>
9 <%= l(:label_version) %> <%= link_to @annotate.content.version, :action => 'index', :page => @page.title, :version => @annotate.content.version %>
10 <em>(<%= @annotate.content.author ? @annotate.content.author.name : "anonyme" %>, <%= format_time(@annotate.content.updated_on) %>)</em>
10 <em>(<%= @annotate.content.author ? @annotate.content.author.name : "anonyme" %>, <%= format_time(@annotate.content.updated_on) %>)</em>
11 </p>
11 </p>
12
12
13 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
13 <% colors = Hash.new {|k,v| k[v] = (k.size % 12) } %>
14
14
15 <table class="filecontent annotate CodeRay ">
15 <table class="filecontent annotate CodeRay ">
16 <tbody>
16 <tbody>
17 <% line_num = 1 %>
17 <% line_num = 1 %>
18 <% @annotate.lines.each do |line| -%>
18 <% @annotate.lines.each do |line| -%>
19 <tr class="bloc-<%= colors[line[0]] %>">
19 <tr class="bloc-<%= colors[line[0]] %>">
20 <th class="line-num"><%= line_num %></th>
20 <th class="line-num"><%= line_num %></th>
21 <td class="revision"><%= link_to line[0], :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title, :version => line[0] %></td>
21 <td class="revision"><%= link_to line[0], :controller => 'wiki', :action => 'index', :id => @project, :page => @page.title, :version => line[0] %></td>
22 <td class="author"><%= h(line[1]) %></td>
22 <td class="author"><%= h(line[1]) %></td>
23 <td class="line-code"><pre><%= line[2] %></pre></td>
23 <td class="line-code"><pre><%=h line[2] %></pre></td>
24 </tr>
24 </tr>
25 <% line_num += 1 %>
25 <% line_num += 1 %>
26 <% end -%>
26 <% end -%>
27 </tbody>
27 </tbody>
28 </table>
28 </table>
29
29
30 <% content_for :header_tags do %>
30 <% content_for :header_tags do %>
31 <%= stylesheet_link_tag 'scm' %>
31 <%= stylesheet_link_tag 'scm' %>
32 <% end %>
32 <% end %>
@@ -1,1172 +1,1175
1 # vim:ts=4:sw=4:
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
7 # License:: BSD
8 #
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
10 #
11 # Based on (and also inspired by) both:
11 # Based on (and also inspired by) both:
12 #
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
15 #
16 #
16 #
17
17
18 # = RedCloth
18 # = RedCloth
19 #
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
23 #
24 # RedCloth users are encouraged to use Textile if they are generating
24 # RedCloth users are encouraged to use Textile if they are generating
25 # HTML and to use Markdown if others will be viewing the plain text.
25 # HTML and to use Markdown if others will be viewing the plain text.
26 #
26 #
27 # == What is Textile?
27 # == What is Textile?
28 #
28 #
29 # Textile is a simple formatting style for text
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
30 # documents, loosely based on some HTML conventions.
31 #
31 #
32 # == Sample Textile Text
32 # == Sample Textile Text
33 #
33 #
34 # h2. This is a title
34 # h2. This is a title
35 #
35 #
36 # h3. This is a subhead
36 # h3. This is a subhead
37 #
37 #
38 # This is a bit of paragraph.
38 # This is a bit of paragraph.
39 #
39 #
40 # bq. This is a blockquote.
40 # bq. This is a blockquote.
41 #
41 #
42 # = Writing Textile
42 # = Writing Textile
43 #
43 #
44 # A Textile document consists of paragraphs. Paragraphs
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
46 # to the beginning of the paragraph.
47 #
47 #
48 # h[n]. Header of size [n].
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
49 # bq. Blockquote.
50 # # Numeric list.
50 # # Numeric list.
51 # * Bulleted list.
51 # * Bulleted list.
52 #
52 #
53 # == Quick Phrase Modifiers
53 # == Quick Phrase Modifiers
54 #
54 #
55 # Quick phrase modifiers are also included, to allow formatting
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
56 # of small portions of text within a paragraph.
57 #
57 #
58 # \_emphasis\_
58 # \_emphasis\_
59 # \_\_italicized\_\_
59 # \_\_italicized\_\_
60 # \*strong\*
60 # \*strong\*
61 # \*\*bold\*\*
61 # \*\*bold\*\*
62 # ??citation??
62 # ??citation??
63 # -deleted text-
63 # -deleted text-
64 # +inserted text+
64 # +inserted text+
65 # ^superscript^
65 # ^superscript^
66 # ~subscript~
66 # ~subscript~
67 # @code@
67 # @code@
68 # %(classname)span%
68 # %(classname)span%
69 #
69 #
70 # ==notextile== (leave text alone)
70 # ==notextile== (leave text alone)
71 #
71 #
72 # == Links
72 # == Links
73 #
73 #
74 # To make a hypertext link, put the link text in "quotation
74 # To make a hypertext link, put the link text in "quotation
75 # marks" followed immediately by a colon and the URL of the link.
75 # marks" followed immediately by a colon and the URL of the link.
76 #
76 #
77 # Optional: text in (parentheses) following the link text,
77 # Optional: text in (parentheses) following the link text,
78 # but before the closing quotation mark, will become a Title
78 # but before the closing quotation mark, will become a Title
79 # attribute for the link, visible as a tool tip when a cursor is above it.
79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 #
80 #
81 # Example:
81 # Example:
82 #
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
84 #
85 # Will become:
85 # Will become:
86 #
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
88 #
89 # == Images
89 # == Images
90 #
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
92 #
93 # Optional: text that immediately follows the URL in (parentheses) will
93 # Optional: text that immediately follows the URL in (parentheses) will
94 # be used as the Alt text for the image. Images on the web should always
94 # be used as the Alt text for the image. Images on the web should always
95 # have descriptive Alt text for the benefit of readers using non-graphical
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
96 # browsers.
97 #
97 #
98 # Optional: place a colon followed by a URL immediately after the
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
99 # closing ! to make the image into a link.
100 #
100 #
101 # Example:
101 # Example:
102 #
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
104 #
105 # Will become:
105 # Will become:
106 #
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
108 #
109 # With a link:
109 # With a link:
110 #
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
112 #
113 # Will become:
113 # Will become:
114 #
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
116 #
117 # == Defining Acronyms
117 # == Defining Acronyms
118 #
118 #
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 # this should be used at least once for each acronym in documents where they appear.
121 # this should be used at least once for each acronym in documents where they appear.
122 #
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
124 # immediately following the acronym.
125 #
125 #
126 # Example:
126 # Example:
127 #
127 #
128 # ACLU(American Civil Liberties Union)
128 # ACLU(American Civil Liberties Union)
129 #
129 #
130 # Will become:
130 # Will become:
131 #
131 #
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 #
133 #
134 # == Adding Tables
134 # == Adding Tables
135 #
135 #
136 # In Textile, simple tables can be added by seperating each column by
136 # In Textile, simple tables can be added by seperating each column by
137 # a pipe.
137 # a pipe.
138 #
138 #
139 # |a|simple|table|row|
139 # |a|simple|table|row|
140 # |And|Another|table|row|
140 # |And|Another|table|row|
141 #
141 #
142 # Attributes are defined by style definitions in parentheses.
142 # Attributes are defined by style definitions in parentheses.
143 #
143 #
144 # table(border:1px solid black).
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
145 # (background:#ddd;color:red). |{}| | | |
146 #
146 #
147 # == Using RedCloth
147 # == Using RedCloth
148 #
148 #
149 # RedCloth is simply an extension of the String class, which can handle
149 # RedCloth is simply an extension of the String class, which can handle
150 # Textile formatting. Use it like a String and output HTML with its
150 # Textile formatting. Use it like a String and output HTML with its
151 # RedCloth#to_html method.
151 # RedCloth#to_html method.
152 #
152 #
153 # doc = RedCloth.new "
153 # doc = RedCloth.new "
154 #
154 #
155 # h2. Test document
155 # h2. Test document
156 #
156 #
157 # Just a simple test."
157 # Just a simple test."
158 #
158 #
159 # puts doc.to_html
159 # puts doc.to_html
160 #
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
163 # formatting, to boost speed and limit the processor:
164 #
164 #
165 # class RedCloth::Textile.new( str )
165 # class RedCloth::Textile.new( str )
166
166
167 class RedCloth3 < String
167 class RedCloth3 < String
168
168
169 VERSION = '3.0.4'
169 VERSION = '3.0.4'
170 DEFAULT_RULES = [:textile, :markdown]
170 DEFAULT_RULES = [:textile, :markdown]
171
171
172 #
172 #
173 # Two accessor for setting security restrictions.
173 # Two accessor for setting security restrictions.
174 #
174 #
175 # This is a nice thing if you're using RedCloth for
175 # This is a nice thing if you're using RedCloth for
176 # formatting in public places (e.g. Wikis) where you
176 # formatting in public places (e.g. Wikis) where you
177 # don't want users to abuse HTML for bad things.
177 # don't want users to abuse HTML for bad things.
178 #
178 #
179 # If +:filter_html+ is set, HTML which wasn't
179 # If +:filter_html+ is set, HTML which wasn't
180 # created by the Textile processor will be escaped.
180 # created by the Textile processor will be escaped.
181 #
181 #
182 # If +:filter_styles+ is set, it will also disable
182 # If +:filter_styles+ is set, it will also disable
183 # the style markup specifier. ('{color: red}')
183 # the style markup specifier. ('{color: red}')
184 #
184 #
185 attr_accessor :filter_html, :filter_styles
185 attr_accessor :filter_html, :filter_styles
186
186
187 #
187 #
188 # Accessor for toggling hard breaks.
188 # Accessor for toggling hard breaks.
189 #
189 #
190 # If +:hard_breaks+ is set, single newlines will
190 # If +:hard_breaks+ is set, single newlines will
191 # be converted to HTML break tags. This is the
191 # be converted to HTML break tags. This is the
192 # default behavior for traditional RedCloth.
192 # default behavior for traditional RedCloth.
193 #
193 #
194 attr_accessor :hard_breaks
194 attr_accessor :hard_breaks
195
195
196 # Accessor for toggling lite mode.
196 # Accessor for toggling lite mode.
197 #
197 #
198 # In lite mode, block-level rules are ignored. This means
198 # In lite mode, block-level rules are ignored. This means
199 # that tables, paragraphs, lists, and such aren't available.
199 # that tables, paragraphs, lists, and such aren't available.
200 # Only the inline markup for bold, italics, entities and so on.
200 # Only the inline markup for bold, italics, entities and so on.
201 #
201 #
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r.to_html
203 # r.to_html
204 # #=> "And then? She <strong>fell</strong>!"
204 # #=> "And then? She <strong>fell</strong>!"
205 #
205 #
206 attr_accessor :lite_mode
206 attr_accessor :lite_mode
207
207
208 #
208 #
209 # Accessor for toggling span caps.
209 # Accessor for toggling span caps.
210 #
210 #
211 # Textile places `span' tags around capitalized
211 # Textile places `span' tags around capitalized
212 # words by default, but this wreaks havoc on Wikis.
212 # words by default, but this wreaks havoc on Wikis.
213 # If +:no_span_caps+ is set, this will be
213 # If +:no_span_caps+ is set, this will be
214 # suppressed.
214 # suppressed.
215 #
215 #
216 attr_accessor :no_span_caps
216 attr_accessor :no_span_caps
217
217
218 #
218 #
219 # Establishes the markup predence. Available rules include:
219 # Establishes the markup predence. Available rules include:
220 #
220 #
221 # == Textile Rules
221 # == Textile Rules
222 #
222 #
223 # The following textile rules can be set individually. Or add the complete
223 # The following textile rules can be set individually. Or add the complete
224 # set of rules with the single :textile rule, which supplies the rule set in
224 # set of rules with the single :textile rule, which supplies the rule set in
225 # the following precedence:
225 # the following precedence:
226 #
226 #
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # block_textile_table:: Textile table block structures
228 # block_textile_table:: Textile table block structures
229 # block_textile_lists:: Textile list structures
229 # block_textile_lists:: Textile list structures
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # inline_textile_image:: Textile inline images
231 # inline_textile_image:: Textile inline images
232 # inline_textile_link:: Textile inline links
232 # inline_textile_link:: Textile inline links
233 # inline_textile_span:: Textile inline spans
233 # inline_textile_span:: Textile inline spans
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 #
235 #
236 # == Markdown
236 # == Markdown
237 #
237 #
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # block_markdown_setext:: Markdown setext headers
239 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_atx:: Markdown atx headers
240 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_rule:: Markdown horizontal rules
241 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_bq:: Markdown blockquotes
242 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_lists:: Markdown lists
243 # block_markdown_lists:: Markdown lists
244 # inline_markdown_link:: Markdown links
244 # inline_markdown_link:: Markdown links
245 attr_accessor :rules
245 attr_accessor :rules
246
246
247 # Returns a new RedCloth object, based on _string_ and
247 # Returns a new RedCloth object, based on _string_ and
248 # enforcing all the included _restrictions_.
248 # enforcing all the included _restrictions_.
249 #
249 #
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r.to_html
251 # r.to_html
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 #
253 #
254 def initialize( string, restrictions = [] )
254 def initialize( string, restrictions = [] )
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 super( string )
256 super( string )
257 end
257 end
258
258
259 #
259 #
260 # Generates HTML from the Textile contents.
260 # Generates HTML from the Textile contents.
261 #
261 #
262 # r = RedCloth.new( "And then? She *fell*!" )
262 # r = RedCloth.new( "And then? She *fell*!" )
263 # r.to_html( true )
263 # r.to_html( true )
264 # #=>"And then? She <strong>fell</strong>!"
264 # #=>"And then? She <strong>fell</strong>!"
265 #
265 #
266 def to_html( *rules )
266 def to_html( *rules )
267 rules = DEFAULT_RULES if rules.empty?
267 rules = DEFAULT_RULES if rules.empty?
268 # make our working copy
268 # make our working copy
269 text = self.dup
269 text = self.dup
270
270
271 @urlrefs = {}
271 @urlrefs = {}
272 @shelf = []
272 @shelf = []
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 :block_markdown_bq, :block_markdown_lists,
277 :block_markdown_bq, :block_markdown_lists,
278 :inline_markdown_reflink, :inline_markdown_link]
278 :inline_markdown_reflink, :inline_markdown_link]
279 @rules = rules.collect do |rule|
279 @rules = rules.collect do |rule|
280 case rule
280 case rule
281 when :markdown
281 when :markdown
282 markdown_rules
282 markdown_rules
283 when :textile
283 when :textile
284 textile_rules
284 textile_rules
285 else
285 else
286 rule
286 rule
287 end
287 end
288 end.flatten
288 end.flatten
289
289
290 # standard clean up
290 # standard clean up
291 incoming_entities text
291 incoming_entities text
292 clean_white_space text
292 clean_white_space text
293
293
294 # start processor
294 # start processor
295 @pre_list = []
295 @pre_list = []
296 rip_offtags text
296 rip_offtags text
297 no_textile text
297 no_textile text
298 escape_html_tags text
298 escape_html_tags text
299 hard_break text
299 hard_break text
300 unless @lite_mode
300 unless @lite_mode
301 refs text
301 refs text
302 # need to do this before text is split by #blocks
302 # need to do this before text is split by #blocks
303 block_textile_quotes text
303 block_textile_quotes text
304 blocks text
304 blocks text
305 end
305 end
306 inline text
306 inline text
307 smooth_offtags text
307 smooth_offtags text
308
308
309 retrieve text
309 retrieve text
310
310
311 text.gsub!( /<\/?notextile>/, '' )
311 text.gsub!( /<\/?notextile>/, '' )
312 text.gsub!( /x%x%/, '&#38;' )
312 text.gsub!( /x%x%/, '&#38;' )
313 clean_html text if filter_html
313 clean_html text if filter_html
314 text.strip!
314 text.strip!
315 text
315 text
316
316
317 end
317 end
318
318
319 #######
319 #######
320 private
320 private
321 #######
321 #######
322 #
322 #
323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 # (from PyTextile)
324 # (from PyTextile)
325 #
325 #
326 TEXTILE_TAGS =
326 TEXTILE_TAGS =
327
327
328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333
333
334 collect! do |a, b|
334 collect! do |a, b|
335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 end
336 end
337
337
338 #
338 #
339 # Regular expressions to convert to HTML.
339 # Regular expressions to convert to HTML.
340 #
340 #
341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 A_VLGN = /[\-^~]/
342 A_VLGN = /[\-^~]/
343 C_CLAS = '(?:\([^)]+\))'
343 C_CLAS = '(?:\([^)]+\))'
344 C_LNGE = '(?:\[[^\[\]]+\])'
344 C_LNGE = '(?:\[[^\[\]]+\])'
345 C_STYL = '(?:\{[^}]+\})'
345 C_STYL = '(?:\{[^}]+\})'
346 S_CSPN = '(?:\\\\\d+)'
346 S_CSPN = '(?:\\\\\d+)'
347 S_RSPN = '(?:/\d+)'
347 S_RSPN = '(?:/\d+)'
348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356
356
357 # Text markup tags, don't conflict with block tags
357 # Text markup tags, don't conflict with block tags
358 SIMPLE_HTML_TAGS = [
358 SIMPLE_HTML_TAGS = [
359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 ]
362 ]
363
363
364 QTAGS = [
364 QTAGS = [
365 ['**', 'b', :limit],
365 ['**', 'b', :limit],
366 ['*', 'strong', :limit],
366 ['*', 'strong', :limit],
367 ['??', 'cite', :limit],
367 ['??', 'cite', :limit],
368 ['-', 'del', :limit],
368 ['-', 'del', :limit],
369 ['__', 'i', :limit],
369 ['__', 'i', :limit],
370 ['_', 'em', :limit],
370 ['_', 'em', :limit],
371 ['%', 'span', :limit],
371 ['%', 'span', :limit],
372 ['+', 'ins', :limit],
372 ['+', 'ins', :limit],
373 ['^', 'sup', :limit],
373 ['^', 'sup', :limit],
374 ['~', 'sub', :limit]
374 ['~', 'sub', :limit]
375 ]
375 ]
376 QTAGS.collect! do |rc, ht, rtype|
376 QTAGS.collect! do |rc, ht, rtype|
377 rcq = Regexp::quote rc
377 rcq = Regexp::quote rc
378 re =
378 re =
379 case rtype
379 case rtype
380 when :limit
380 when :limit
381 /(^|[>\s\(])
381 /(^|[>\s\(])
382 (#{rcq})
382 (#{rcq})
383 (#{C})
383 (#{C})
384 (?::(\S+?))?
384 (?::(\S+?))?
385 ([^\s\-].*?[^\s\-]|\w)
385 ([^\s\-].*?[^\s\-]|\w)
386 #{rcq}
386 #{rcq}
387 (?=[[:punct:]]|\s|\)|$)/x
387 (?=[[:punct:]]|\s|\)|$)/x
388 else
388 else
389 /(#{rcq})
389 /(#{rcq})
390 (#{C})
390 (#{C})
391 (?::(\S+))?
391 (?::(\S+))?
392 ([^\s\-].*?[^\s\-]|\w)
392 ([^\s\-].*?[^\s\-]|\w)
393 #{rcq}/xm
393 #{rcq}/xm
394 end
394 end
395 [rc, ht, re, rtype]
395 [rc, ht, re, rtype]
396 end
396 end
397
397
398 # Elements to handle
398 # Elements to handle
399 GLYPHS = [
399 GLYPHS = [
400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
403 # [ /\'/, '&#8216;' ], # single opening
403 # [ /\'/, '&#8216;' ], # single opening
404 # [ /</, '&lt;' ], # less-than
404 # [ /</, '&lt;' ], # less-than
405 # [ />/, '&gt;' ], # greater-than
405 # [ />/, '&gt;' ], # greater-than
406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
409 # [ /"/, '&#8220;' ], # double opening
409 # [ /"/, '&#8220;' ], # double opening
410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
411 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
411 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
412 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
412 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
415 # [ /\s-\s/, ' &#8211; ' ], # en dash
415 # [ /\s-\s/, ' &#8211; ' ], # en dash
416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
420 ]
420 ]
421
421
422 H_ALGN_VALS = {
422 H_ALGN_VALS = {
423 '<' => 'left',
423 '<' => 'left',
424 '=' => 'center',
424 '=' => 'center',
425 '>' => 'right',
425 '>' => 'right',
426 '<>' => 'justify'
426 '<>' => 'justify'
427 }
427 }
428
428
429 V_ALGN_VALS = {
429 V_ALGN_VALS = {
430 '^' => 'top',
430 '^' => 'top',
431 '-' => 'middle',
431 '-' => 'middle',
432 '~' => 'bottom'
432 '~' => 'bottom'
433 }
433 }
434
434
435 #
435 #
436 # Flexible HTML escaping
436 # Flexible HTML escaping
437 #
437 #
438 def htmlesc( str, mode=:Quotes )
438 def htmlesc( str, mode=:Quotes )
439 if str
439 if str
440 str.gsub!( '&', '&amp;' )
440 str.gsub!( '&', '&amp;' )
441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
443 str.gsub!( '<', '&lt;')
443 str.gsub!( '<', '&lt;')
444 str.gsub!( '>', '&gt;')
444 str.gsub!( '>', '&gt;')
445 end
445 end
446 str
446 str
447 end
447 end
448
448
449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
450 def pgl( text )
450 def pgl( text )
451 GLYPHS.each do |re, resub, tog|
451 #GLYPHS.each do |re, resub, tog|
452 next if tog and method( tog ).call
452 # next if tog and method( tog ).call
453 text.gsub! re, resub
453 # text.gsub! re, resub
454 #end
455 text.gsub!(/\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/) do |m|
456 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
454 end
457 end
455 end
458 end
456
459
457 # Parses Textile attribute lists and builds an HTML attribute string
460 # Parses Textile attribute lists and builds an HTML attribute string
458 def pba( text_in, element = "" )
461 def pba( text_in, element = "" )
459
462
460 return '' unless text_in
463 return '' unless text_in
461
464
462 style = []
465 style = []
463 text = text_in.dup
466 text = text_in.dup
464 if element == 'td'
467 if element == 'td'
465 colspan = $1 if text =~ /\\(\d+)/
468 colspan = $1 if text =~ /\\(\d+)/
466 rowspan = $1 if text =~ /\/(\d+)/
469 rowspan = $1 if text =~ /\/(\d+)/
467 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
470 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
468 end
471 end
469
472
470 style << "#{ $1 };" if not filter_styles and
473 style << "#{ htmlesc $1 };" if not filter_styles and
471 text.sub!( /\{([^}]*)\}/, '' )
474 text.sub!( /\{([^}]*)\}/, '' )
472
475
473 lang = $1 if
476 lang = $1 if
474 text.sub!( /\[([^)]+?)\]/, '' )
477 text.sub!( /\[([^)]+?)\]/, '' )
475
478
476 cls = $1 if
479 cls = $1 if
477 text.sub!( /\(([^()]+?)\)/, '' )
480 text.sub!( /\(([^()]+?)\)/, '' )
478
481
479 style << "padding-left:#{ $1.length }em;" if
482 style << "padding-left:#{ $1.length }em;" if
480 text.sub!( /([(]+)/, '' )
483 text.sub!( /([(]+)/, '' )
481
484
482 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
485 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
483
486
484 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
487 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
485
488
486 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
489 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
487
490
488 atts = ''
491 atts = ''
489 atts << " style=\"#{ style.join }\"" unless style.empty?
492 atts << " style=\"#{ style.join }\"" unless style.empty?
490 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
493 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
491 atts << " lang=\"#{ lang }\"" if lang
494 atts << " lang=\"#{ lang }\"" if lang
492 atts << " id=\"#{ id }\"" if id
495 atts << " id=\"#{ id }\"" if id
493 atts << " colspan=\"#{ colspan }\"" if colspan
496 atts << " colspan=\"#{ colspan }\"" if colspan
494 atts << " rowspan=\"#{ rowspan }\"" if rowspan
497 atts << " rowspan=\"#{ rowspan }\"" if rowspan
495
498
496 atts
499 atts
497 end
500 end
498
501
499 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
502 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
500
503
501 # Parses a Textile table block, building HTML from the result.
504 # Parses a Textile table block, building HTML from the result.
502 def block_textile_table( text )
505 def block_textile_table( text )
503 text.gsub!( TABLE_RE ) do |matches|
506 text.gsub!( TABLE_RE ) do |matches|
504
507
505 tatts, fullrow = $~[1..2]
508 tatts, fullrow = $~[1..2]
506 tatts = pba( tatts, 'table' )
509 tatts = pba( tatts, 'table' )
507 tatts = shelve( tatts ) if tatts
510 tatts = shelve( tatts ) if tatts
508 rows = []
511 rows = []
509
512
510 fullrow.each_line do |row|
513 fullrow.each_line do |row|
511 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
514 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
512 cells = []
515 cells = []
513 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
516 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
514 next if cell == '|'
517 next if cell == '|'
515 ctyp = 'd'
518 ctyp = 'd'
516 ctyp = 'h' if cell =~ /^_/
519 ctyp = 'h' if cell =~ /^_/
517
520
518 catts = ''
521 catts = ''
519 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
522 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
520
523
521 catts = shelve( catts ) if catts
524 catts = shelve( catts ) if catts
522 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
525 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
523 end
526 end
524 ratts = shelve( ratts ) if ratts
527 ratts = shelve( ratts ) if ratts
525 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
528 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
526 end
529 end
527 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
530 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
528 end
531 end
529 end
532 end
530
533
531 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
534 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
532 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
535 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
533
536
534 # Parses Textile lists and generates HTML
537 # Parses Textile lists and generates HTML
535 def block_textile_lists( text )
538 def block_textile_lists( text )
536 text.gsub!( LISTS_RE ) do |match|
539 text.gsub!( LISTS_RE ) do |match|
537 lines = match.split( /\n/ )
540 lines = match.split( /\n/ )
538 last_line = -1
541 last_line = -1
539 depth = []
542 depth = []
540 lines.each_with_index do |line, line_id|
543 lines.each_with_index do |line, line_id|
541 if line =~ LISTS_CONTENT_RE
544 if line =~ LISTS_CONTENT_RE
542 tl,atts,content = $~[1..3]
545 tl,atts,content = $~[1..3]
543 if depth.last
546 if depth.last
544 if depth.last.length > tl.length
547 if depth.last.length > tl.length
545 (depth.length - 1).downto(0) do |i|
548 (depth.length - 1).downto(0) do |i|
546 break if depth[i].length == tl.length
549 break if depth[i].length == tl.length
547 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
550 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
548 depth.pop
551 depth.pop
549 end
552 end
550 end
553 end
551 if depth.last and depth.last.length == tl.length
554 if depth.last and depth.last.length == tl.length
552 lines[line_id - 1] << '</li>'
555 lines[line_id - 1] << '</li>'
553 end
556 end
554 end
557 end
555 unless depth.last == tl
558 unless depth.last == tl
556 depth << tl
559 depth << tl
557 atts = pba( atts )
560 atts = pba( atts )
558 atts = shelve( atts ) if atts
561 atts = shelve( atts ) if atts
559 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
562 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
560 else
563 else
561 lines[line_id] = "\t\t<li>#{ content }"
564 lines[line_id] = "\t\t<li>#{ content }"
562 end
565 end
563 last_line = line_id
566 last_line = line_id
564
567
565 else
568 else
566 last_line = line_id
569 last_line = line_id
567 end
570 end
568 if line_id - last_line > 1 or line_id == lines.length - 1
571 if line_id - last_line > 1 or line_id == lines.length - 1
569 depth.delete_if do |v|
572 depth.delete_if do |v|
570 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
573 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
571 end
574 end
572 end
575 end
573 end
576 end
574 lines.join( "\n" )
577 lines.join( "\n" )
575 end
578 end
576 end
579 end
577
580
578 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
581 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
579 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
582 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
580
583
581 def block_textile_quotes( text )
584 def block_textile_quotes( text )
582 text.gsub!( QUOTES_RE ) do |match|
585 text.gsub!( QUOTES_RE ) do |match|
583 lines = match.split( /\n/ )
586 lines = match.split( /\n/ )
584 quotes = ''
587 quotes = ''
585 indent = 0
588 indent = 0
586 lines.each do |line|
589 lines.each do |line|
587 line =~ QUOTES_CONTENT_RE
590 line =~ QUOTES_CONTENT_RE
588 bq,content = $1, $2
591 bq,content = $1, $2
589 l = bq.count('>')
592 l = bq.count('>')
590 if l != indent
593 if l != indent
591 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
594 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
592 indent = l
595 indent = l
593 end
596 end
594 quotes << (content + "\n")
597 quotes << (content + "\n")
595 end
598 end
596 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
599 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
597 quotes
600 quotes
598 end
601 end
599 end
602 end
600
603
601 CODE_RE = /(\W)
604 CODE_RE = /(\W)
602 @
605 @
603 (?:\|(\w+?)\|)?
606 (?:\|(\w+?)\|)?
604 (.+?)
607 (.+?)
605 @
608 @
606 (?=\W)/x
609 (?=\W)/x
607
610
608 def inline_textile_code( text )
611 def inline_textile_code( text )
609 text.gsub!( CODE_RE ) do |m|
612 text.gsub!( CODE_RE ) do |m|
610 before,lang,code,after = $~[1..4]
613 before,lang,code,after = $~[1..4]
611 lang = " lang=\"#{ lang }\"" if lang
614 lang = " lang=\"#{ lang }\"" if lang
612 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
615 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
613 end
616 end
614 end
617 end
615
618
616 def lT( text )
619 def lT( text )
617 text =~ /\#$/ ? 'o' : 'u'
620 text =~ /\#$/ ? 'o' : 'u'
618 end
621 end
619
622
620 def hard_break( text )
623 def hard_break( text )
621 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
624 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
622 end
625 end
623
626
624 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
627 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
625
628
626 def blocks( text, deep_code = false )
629 def blocks( text, deep_code = false )
627 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
630 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
628 plain = blk !~ /\A[#*> ]/
631 plain = blk !~ /\A[#*> ]/
629
632
630 # skip blocks that are complex HTML
633 # skip blocks that are complex HTML
631 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
634 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
632 blk
635 blk
633 else
636 else
634 # search for indentation levels
637 # search for indentation levels
635 blk.strip!
638 blk.strip!
636 if blk.empty?
639 if blk.empty?
637 blk
640 blk
638 else
641 else
639 code_blk = nil
642 code_blk = nil
640 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
643 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
641 flush_left iblk
644 flush_left iblk
642 blocks iblk, plain
645 blocks iblk, plain
643 iblk.gsub( /^(\S)/, "\t\\1" )
646 iblk.gsub( /^(\S)/, "\t\\1" )
644 if plain
647 if plain
645 code_blk = iblk; ""
648 code_blk = iblk; ""
646 else
649 else
647 iblk
650 iblk
648 end
651 end
649 end
652 end
650
653
651 block_applied = 0
654 block_applied = 0
652 @rules.each do |rule_name|
655 @rules.each do |rule_name|
653 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
656 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
654 end
657 end
655 if block_applied.zero?
658 if block_applied.zero?
656 if deep_code
659 if deep_code
657 blk = "\t<pre><code>#{ blk }</code></pre>"
660 blk = "\t<pre><code>#{ blk }</code></pre>"
658 else
661 else
659 blk = "\t<p>#{ blk }</p>"
662 blk = "\t<p>#{ blk }</p>"
660 end
663 end
661 end
664 end
662 # hard_break blk
665 # hard_break blk
663 blk + "\n#{ code_blk }"
666 blk + "\n#{ code_blk }"
664 end
667 end
665 end
668 end
666
669
667 end.join( "\n\n" ) )
670 end.join( "\n\n" ) )
668 end
671 end
669
672
670 def textile_bq( tag, atts, cite, content )
673 def textile_bq( tag, atts, cite, content )
671 cite, cite_title = check_refs( cite )
674 cite, cite_title = check_refs( cite )
672 cite = " cite=\"#{ cite }\"" if cite
675 cite = " cite=\"#{ cite }\"" if cite
673 atts = shelve( atts ) if atts
676 atts = shelve( atts ) if atts
674 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
677 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
675 end
678 end
676
679
677 def textile_p( tag, atts, cite, content )
680 def textile_p( tag, atts, cite, content )
678 atts = shelve( atts ) if atts
681 atts = shelve( atts ) if atts
679 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
682 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
680 end
683 end
681
684
682 alias textile_h1 textile_p
685 alias textile_h1 textile_p
683 alias textile_h2 textile_p
686 alias textile_h2 textile_p
684 alias textile_h3 textile_p
687 alias textile_h3 textile_p
685 alias textile_h4 textile_p
688 alias textile_h4 textile_p
686 alias textile_h5 textile_p
689 alias textile_h5 textile_p
687 alias textile_h6 textile_p
690 alias textile_h6 textile_p
688
691
689 def textile_fn_( tag, num, atts, cite, content )
692 def textile_fn_( tag, num, atts, cite, content )
690 atts << " id=\"fn#{ num }\" class=\"footnote\""
693 atts << " id=\"fn#{ num }\" class=\"footnote\""
691 content = "<sup>#{ num }</sup> #{ content }"
694 content = "<sup>#{ num }</sup> #{ content }"
692 atts = shelve( atts ) if atts
695 atts = shelve( atts ) if atts
693 "\t<p#{ atts }>#{ content }</p>"
696 "\t<p#{ atts }>#{ content }</p>"
694 end
697 end
695
698
696 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
699 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
697
700
698 def block_textile_prefix( text )
701 def block_textile_prefix( text )
699 if text =~ BLOCK_RE
702 if text =~ BLOCK_RE
700 tag,tagpre,num,atts,cite,content = $~[1..6]
703 tag,tagpre,num,atts,cite,content = $~[1..6]
701 atts = pba( atts )
704 atts = pba( atts )
702
705
703 # pass to prefix handler
706 # pass to prefix handler
704 if respond_to? "textile_#{ tag }", true
707 if respond_to? "textile_#{ tag }", true
705 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
708 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
706 elsif respond_to? "textile_#{ tagpre }_", true
709 elsif respond_to? "textile_#{ tagpre }_", true
707 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
710 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
708 end
711 end
709 end
712 end
710 end
713 end
711
714
712 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
715 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
713 def block_markdown_setext( text )
716 def block_markdown_setext( text )
714 if text =~ SETEXT_RE
717 if text =~ SETEXT_RE
715 tag = if $2 == "="; "h1"; else; "h2"; end
718 tag = if $2 == "="; "h1"; else; "h2"; end
716 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
719 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
717 blocks cont
720 blocks cont
718 text.replace( blk + cont )
721 text.replace( blk + cont )
719 end
722 end
720 end
723 end
721
724
722 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
725 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
723 [ ]*
726 [ ]*
724 (.+?) # $2 = Header text
727 (.+?) # $2 = Header text
725 [ ]*
728 [ ]*
726 \#* # optional closing #'s (not counted)
729 \#* # optional closing #'s (not counted)
727 $/x
730 $/x
728 def block_markdown_atx( text )
731 def block_markdown_atx( text )
729 if text =~ ATX_RE
732 if text =~ ATX_RE
730 tag = "h#{ $1.length }"
733 tag = "h#{ $1.length }"
731 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
734 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
732 blocks cont
735 blocks cont
733 text.replace( blk + cont )
736 text.replace( blk + cont )
734 end
737 end
735 end
738 end
736
739
737 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
740 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
738
741
739 def block_markdown_bq( text )
742 def block_markdown_bq( text )
740 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
743 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
741 blk.gsub!( /^ *> ?/, '' )
744 blk.gsub!( /^ *> ?/, '' )
742 flush_left blk
745 flush_left blk
743 blocks blk
746 blocks blk
744 blk.gsub!( /^(\S)/, "\t\\1" )
747 blk.gsub!( /^(\S)/, "\t\\1" )
745 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
748 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
746 end
749 end
747 end
750 end
748
751
749 MARKDOWN_RULE_RE = /^(#{
752 MARKDOWN_RULE_RE = /^(#{
750 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
753 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
751 })$/
754 })$/
752
755
753 def block_markdown_rule( text )
756 def block_markdown_rule( text )
754 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
757 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
755 "<hr />"
758 "<hr />"
756 end
759 end
757 end
760 end
758
761
759 # XXX TODO XXX
762 # XXX TODO XXX
760 def block_markdown_lists( text )
763 def block_markdown_lists( text )
761 end
764 end
762
765
763 def inline_textile_span( text )
766 def inline_textile_span( text )
764 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
767 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
765 text.gsub!( qtag_re ) do |m|
768 text.gsub!( qtag_re ) do |m|
766
769
767 case rtype
770 case rtype
768 when :limit
771 when :limit
769 sta,qtag,atts,cite,content = $~[1..5]
772 sta,qtag,atts,cite,content = $~[1..5]
770 else
773 else
771 qtag,atts,cite,content = $~[1..4]
774 qtag,atts,cite,content = $~[1..4]
772 sta = ''
775 sta = ''
773 end
776 end
774 atts = pba( atts )
777 atts = pba( atts )
775 atts << " cite=\"#{ cite }\"" if cite
778 atts << " cite=\"#{ cite }\"" if cite
776 atts = shelve( atts ) if atts
779 atts = shelve( atts ) if atts
777
780
778 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
781 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
779
782
780 end
783 end
781 end
784 end
782 end
785 end
783
786
784 LINK_RE = /
787 LINK_RE = /
785 ([\s\[{(]|[#{PUNCT}])? # $pre
788 ([\s\[{(]|[#{PUNCT}])? # $pre
786 " # start
789 " # start
787 (#{C}) # $atts
790 (#{C}) # $atts
788 ([^"\n]+?) # $text
791 ([^"\n]+?) # $text
789 \s?
792 \s?
790 (?:\(([^)]+?)\)(?="))? # $title
793 (?:\(([^)]+?)\)(?="))? # $title
791 ":
794 ":
792 ([\w\/]\S+?) # $url
795 ([\w\/]\S+?) # $url
793 (\/)? # $slash
796 (\/)? # $slash
794 ([^\w\=\/;\(\)]*?) # $post
797 ([^\w\=\/;\(\)]*?) # $post
795 (?=<|\s|$)
798 (?=<|\s|$)
796 /x
799 /x
797 #"
800 #"
798 def inline_textile_link( text )
801 def inline_textile_link( text )
799 text.gsub!( LINK_RE ) do |m|
802 text.gsub!( LINK_RE ) do |m|
800 pre,atts,text,title,url,slash,post = $~[1..7]
803 pre,atts,text,title,url,slash,post = $~[1..7]
801
804
802 url, url_title = check_refs( url )
805 url, url_title = check_refs( url )
803 title ||= url_title
806 title ||= url_title
804
807
805 # Idea below : an URL with unbalanced parethesis and
808 # Idea below : an URL with unbalanced parethesis and
806 # ending by ')' is put into external parenthesis
809 # ending by ')' is put into external parenthesis
807 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
810 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
808 url=url[0..-2] # discard closing parenth from url
811 url=url[0..-2] # discard closing parenth from url
809 post = ")"+post # add closing parenth to post
812 post = ")"+post # add closing parenth to post
810 end
813 end
811 atts = pba( atts )
814 atts = pba( atts )
812 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
815 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
813 atts << " title=\"#{ title }\"" if title
816 atts << " title=\"#{ htmlesc title }\"" if title
814 atts = shelve( atts ) if atts
817 atts = shelve( atts ) if atts
815
818
816 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
819 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
817
820
818 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
821 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
819 end
822 end
820 end
823 end
821
824
822 MARKDOWN_REFLINK_RE = /
825 MARKDOWN_REFLINK_RE = /
823 \[([^\[\]]+)\] # $text
826 \[([^\[\]]+)\] # $text
824 [ ]? # opt. space
827 [ ]? # opt. space
825 (?:\n[ ]*)? # one optional newline followed by spaces
828 (?:\n[ ]*)? # one optional newline followed by spaces
826 \[(.*?)\] # $id
829 \[(.*?)\] # $id
827 /x
830 /x
828
831
829 def inline_markdown_reflink( text )
832 def inline_markdown_reflink( text )
830 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
833 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
831 text, id = $~[1..2]
834 text, id = $~[1..2]
832
835
833 if id.empty?
836 if id.empty?
834 url, title = check_refs( text )
837 url, title = check_refs( text )
835 else
838 else
836 url, title = check_refs( id )
839 url, title = check_refs( id )
837 end
840 end
838
841
839 atts = " href=\"#{ url }\""
842 atts = " href=\"#{ url }\""
840 atts << " title=\"#{ title }\"" if title
843 atts << " title=\"#{ title }\"" if title
841 atts = shelve( atts )
844 atts = shelve( atts )
842
845
843 "<a#{ atts }>#{ text }</a>"
846 "<a#{ atts }>#{ text }</a>"
844 end
847 end
845 end
848 end
846
849
847 MARKDOWN_LINK_RE = /
850 MARKDOWN_LINK_RE = /
848 \[([^\[\]]+)\] # $text
851 \[([^\[\]]+)\] # $text
849 \( # open paren
852 \( # open paren
850 [ \t]* # opt space
853 [ \t]* # opt space
851 <?(.+?)>? # $href
854 <?(.+?)>? # $href
852 [ \t]* # opt space
855 [ \t]* # opt space
853 (?: # whole title
856 (?: # whole title
854 (['"]) # $quote
857 (['"]) # $quote
855 (.*?) # $title
858 (.*?) # $title
856 \3 # matching quote
859 \3 # matching quote
857 )? # title is optional
860 )? # title is optional
858 \)
861 \)
859 /x
862 /x
860
863
861 def inline_markdown_link( text )
864 def inline_markdown_link( text )
862 text.gsub!( MARKDOWN_LINK_RE ) do |m|
865 text.gsub!( MARKDOWN_LINK_RE ) do |m|
863 text, url, quote, title = $~[1..4]
866 text, url, quote, title = $~[1..4]
864
867
865 atts = " href=\"#{ url }\""
868 atts = " href=\"#{ url }\""
866 atts << " title=\"#{ title }\"" if title
869 atts << " title=\"#{ title }\"" if title
867 atts = shelve( atts )
870 atts = shelve( atts )
868
871
869 "<a#{ atts }>#{ text }</a>"
872 "<a#{ atts }>#{ text }</a>"
870 end
873 end
871 end
874 end
872
875
873 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
876 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
874 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
877 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
875
878
876 def refs( text )
879 def refs( text )
877 @rules.each do |rule_name|
880 @rules.each do |rule_name|
878 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
881 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
879 end
882 end
880 end
883 end
881
884
882 def refs_textile( text )
885 def refs_textile( text )
883 text.gsub!( TEXTILE_REFS_RE ) do |m|
886 text.gsub!( TEXTILE_REFS_RE ) do |m|
884 flag, url = $~[2..3]
887 flag, url = $~[2..3]
885 @urlrefs[flag.downcase] = [url, nil]
888 @urlrefs[flag.downcase] = [url, nil]
886 nil
889 nil
887 end
890 end
888 end
891 end
889
892
890 def refs_markdown( text )
893 def refs_markdown( text )
891 text.gsub!( MARKDOWN_REFS_RE ) do |m|
894 text.gsub!( MARKDOWN_REFS_RE ) do |m|
892 flag, url = $~[2..3]
895 flag, url = $~[2..3]
893 title = $~[6]
896 title = $~[6]
894 @urlrefs[flag.downcase] = [url, title]
897 @urlrefs[flag.downcase] = [url, title]
895 nil
898 nil
896 end
899 end
897 end
900 end
898
901
899 def check_refs( text )
902 def check_refs( text )
900 ret = @urlrefs[text.downcase] if text
903 ret = @urlrefs[text.downcase] if text
901 ret || [text, nil]
904 ret || [text, nil]
902 end
905 end
903
906
904 IMAGE_RE = /
907 IMAGE_RE = /
905 (<p>|.|^) # start of line?
908 (<p>|.|^) # start of line?
906 \! # opening
909 \! # opening
907 (\<|\=|\>)? # optional alignment atts
910 (\<|\=|\>)? # optional alignment atts
908 (#{C}) # optional style,class atts
911 (#{C}) # optional style,class atts
909 (?:\. )? # optional dot-space
912 (?:\. )? # optional dot-space
910 ([^\s(!]+?) # presume this is the src
913 ([^\s(!]+?) # presume this is the src
911 \s? # optional space
914 \s? # optional space
912 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
915 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
913 \! # closing
916 \! # closing
914 (?::#{ HYPERLINK })? # optional href
917 (?::#{ HYPERLINK })? # optional href
915 /x
918 /x
916
919
917 def inline_textile_image( text )
920 def inline_textile_image( text )
918 text.gsub!( IMAGE_RE ) do |m|
921 text.gsub!( IMAGE_RE ) do |m|
919 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
922 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
920 htmlesc title
923 htmlesc title
921 atts = pba( atts )
924 atts = pba( atts )
922 atts = " src=\"#{ url }\"#{ atts }"
925 atts = " src=\"#{ url }\"#{ atts }"
923 atts << " title=\"#{ title }\"" if title
926 atts << " title=\"#{ title }\"" if title
924 atts << " alt=\"#{ title }\""
927 atts << " alt=\"#{ title }\""
925 # size = @getimagesize($url);
928 # size = @getimagesize($url);
926 # if($size) $atts.= " $size[3]";
929 # if($size) $atts.= " $size[3]";
927
930
928 href, alt_title = check_refs( href ) if href
931 href, alt_title = check_refs( href ) if href
929 url, url_title = check_refs( url )
932 url, url_title = check_refs( url )
930
933
931 out = ''
934 out = ''
932 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
935 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
933 out << "<img#{ shelve( atts ) } />"
936 out << "<img#{ shelve( atts ) } />"
934 out << "</a>#{ href_a1 }#{ href_a2 }" if href
937 out << "</a>#{ href_a1 }#{ href_a2 }" if href
935
938
936 if algn
939 if algn
937 algn = h_align( algn )
940 algn = h_align( algn )
938 if stln == "<p>"
941 if stln == "<p>"
939 out = "<p style=\"float:#{ algn }\">#{ out }"
942 out = "<p style=\"float:#{ algn }\">#{ out }"
940 else
943 else
941 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
944 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
942 end
945 end
943 else
946 else
944 out = stln + out
947 out = stln + out
945 end
948 end
946
949
947 out
950 out
948 end
951 end
949 end
952 end
950
953
951 def shelve( val )
954 def shelve( val )
952 @shelf << val
955 @shelf << val
953 " :redsh##{ @shelf.length }:"
956 " :redsh##{ @shelf.length }:"
954 end
957 end
955
958
956 def retrieve( text )
959 def retrieve( text )
957 @shelf.each_with_index do |r, i|
960 @shelf.each_with_index do |r, i|
958 text.gsub!( " :redsh##{ i + 1 }:", r )
961 text.gsub!( " :redsh##{ i + 1 }:", r )
959 end
962 end
960 end
963 end
961
964
962 def incoming_entities( text )
965 def incoming_entities( text )
963 ## turn any incoming ampersands into a dummy character for now.
966 ## turn any incoming ampersands into a dummy character for now.
964 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
967 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
965 ## implying an incoming html entity, to be skipped
968 ## implying an incoming html entity, to be skipped
966
969
967 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
970 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
968 end
971 end
969
972
970 def no_textile( text )
973 def no_textile( text )
971 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
974 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
972 '\1<notextile>\2</notextile>\3' )
975 '\1<notextile>\2</notextile>\3' )
973 text.gsub!( /^ *==([^=]+.*?)==/m,
976 text.gsub!( /^ *==([^=]+.*?)==/m,
974 '\1<notextile>\2</notextile>\3' )
977 '\1<notextile>\2</notextile>\3' )
975 end
978 end
976
979
977 def clean_white_space( text )
980 def clean_white_space( text )
978 # normalize line breaks
981 # normalize line breaks
979 text.gsub!( /\r\n/, "\n" )
982 text.gsub!( /\r\n/, "\n" )
980 text.gsub!( /\r/, "\n" )
983 text.gsub!( /\r/, "\n" )
981 text.gsub!( /\t/, ' ' )
984 text.gsub!( /\t/, ' ' )
982 text.gsub!( /^ +$/, '' )
985 text.gsub!( /^ +$/, '' )
983 text.gsub!( /\n{3,}/, "\n\n" )
986 text.gsub!( /\n{3,}/, "\n\n" )
984 text.gsub!( /"$/, "\" " )
987 text.gsub!( /"$/, "\" " )
985
988
986 # if entire document is indented, flush
989 # if entire document is indented, flush
987 # to the left side
990 # to the left side
988 flush_left text
991 flush_left text
989 end
992 end
990
993
991 def flush_left( text )
994 def flush_left( text )
992 indt = 0
995 indt = 0
993 if text =~ /^ /
996 if text =~ /^ /
994 while text !~ /^ {#{indt}}\S/
997 while text !~ /^ {#{indt}}\S/
995 indt += 1
998 indt += 1
996 end unless text.empty?
999 end unless text.empty?
997 if indt.nonzero?
1000 if indt.nonzero?
998 text.gsub!( /^ {#{indt}}/, '' )
1001 text.gsub!( /^ {#{indt}}/, '' )
999 end
1002 end
1000 end
1003 end
1001 end
1004 end
1002
1005
1003 def footnote_ref( text )
1006 def footnote_ref( text )
1004 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1007 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1005 '<sup><a href="#fn\1">\1</a></sup>\2' )
1008 '<sup><a href="#fn\1">\1</a></sup>\2' )
1006 end
1009 end
1007
1010
1008 OFFTAGS = /(code|pre|kbd|notextile)/
1011 OFFTAGS = /(code|pre|kbd|notextile)/
1009 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1012 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1010 OFFTAG_OPEN = /<#{ OFFTAGS }/
1013 OFFTAG_OPEN = /<#{ OFFTAGS }/
1011 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1014 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1012 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1015 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1013 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1016 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1014
1017
1015 def glyphs_textile( text, level = 0 )
1018 def glyphs_textile( text, level = 0 )
1016 if text !~ HASTAG_MATCH
1019 if text !~ HASTAG_MATCH
1017 pgl text
1020 pgl text
1018 footnote_ref text
1021 footnote_ref text
1019 else
1022 else
1020 codepre = 0
1023 codepre = 0
1021 text.gsub!( ALLTAG_MATCH ) do |line|
1024 text.gsub!( ALLTAG_MATCH ) do |line|
1022 ## matches are off if we're between <code>, <pre> etc.
1025 ## matches are off if we're between <code>, <pre> etc.
1023 if $1
1026 if $1
1024 if line =~ OFFTAG_OPEN
1027 if line =~ OFFTAG_OPEN
1025 codepre += 1
1028 codepre += 1
1026 elsif line =~ OFFTAG_CLOSE
1029 elsif line =~ OFFTAG_CLOSE
1027 codepre -= 1
1030 codepre -= 1
1028 codepre = 0 if codepre < 0
1031 codepre = 0 if codepre < 0
1029 end
1032 end
1030 elsif codepre.zero?
1033 elsif codepre.zero?
1031 glyphs_textile( line, level + 1 )
1034 glyphs_textile( line, level + 1 )
1032 else
1035 else
1033 htmlesc( line, :NoQuotes )
1036 htmlesc( line, :NoQuotes )
1034 end
1037 end
1035 # p [level, codepre, line]
1038 # p [level, codepre, line]
1036
1039
1037 line
1040 line
1038 end
1041 end
1039 end
1042 end
1040 end
1043 end
1041
1044
1042 def rip_offtags( text )
1045 def rip_offtags( text )
1043 if text =~ /<.*>/
1046 if text =~ /<.*>/
1044 ## strip and encode <pre> content
1047 ## strip and encode <pre> content
1045 codepre, used_offtags = 0, {}
1048 codepre, used_offtags = 0, {}
1046 text.gsub!( OFFTAG_MATCH ) do |line|
1049 text.gsub!( OFFTAG_MATCH ) do |line|
1047 if $3
1050 if $3
1048 offtag, aftertag = $4, $5
1051 offtag, aftertag = $4, $5
1049 codepre += 1
1052 codepre += 1
1050 used_offtags[offtag] = true
1053 used_offtags[offtag] = true
1051 if codepre - used_offtags.length > 0
1054 if codepre - used_offtags.length > 0
1052 htmlesc( line, :NoQuotes )
1055 htmlesc( line, :NoQuotes )
1053 @pre_list.last << line
1056 @pre_list.last << line
1054 line = ""
1057 line = ""
1055 else
1058 else
1056 htmlesc( aftertag, :NoQuotes ) if aftertag
1059 htmlesc( aftertag, :NoQuotes ) if aftertag
1057 line = "<redpre##{ @pre_list.length }>"
1060 line = "<redpre##{ @pre_list.length }>"
1058 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1061 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1059 tag = $1
1062 tag = $1
1060 $2.to_s.match(/(class\=\S+)/i)
1063 $2.to_s.match(/(class\=\S+)/i)
1061 tag << " #{$1}" if $1
1064 tag << " #{$1}" if $1
1062 @pre_list << "<#{ tag }>#{ aftertag }"
1065 @pre_list << "<#{ tag }>#{ aftertag }"
1063 end
1066 end
1064 elsif $1 and codepre > 0
1067 elsif $1 and codepre > 0
1065 if codepre - used_offtags.length > 0
1068 if codepre - used_offtags.length > 0
1066 htmlesc( line, :NoQuotes )
1069 htmlesc( line, :NoQuotes )
1067 @pre_list.last << line
1070 @pre_list.last << line
1068 line = ""
1071 line = ""
1069 end
1072 end
1070 codepre -= 1 unless codepre.zero?
1073 codepre -= 1 unless codepre.zero?
1071 used_offtags = {} if codepre.zero?
1074 used_offtags = {} if codepre.zero?
1072 end
1075 end
1073 line
1076 line
1074 end
1077 end
1075 end
1078 end
1076 text
1079 text
1077 end
1080 end
1078
1081
1079 def smooth_offtags( text )
1082 def smooth_offtags( text )
1080 unless @pre_list.empty?
1083 unless @pre_list.empty?
1081 ## replace <pre> content
1084 ## replace <pre> content
1082 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1085 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1083 end
1086 end
1084 end
1087 end
1085
1088
1086 def inline( text )
1089 def inline( text )
1087 [/^inline_/, /^glyphs_/].each do |meth_re|
1090 [/^inline_/, /^glyphs_/].each do |meth_re|
1088 @rules.each do |rule_name|
1091 @rules.each do |rule_name|
1089 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1092 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1090 end
1093 end
1091 end
1094 end
1092 end
1095 end
1093
1096
1094 def h_align( text )
1097 def h_align( text )
1095 H_ALGN_VALS[text]
1098 H_ALGN_VALS[text]
1096 end
1099 end
1097
1100
1098 def v_align( text )
1101 def v_align( text )
1099 V_ALGN_VALS[text]
1102 V_ALGN_VALS[text]
1100 end
1103 end
1101
1104
1102 def textile_popup_help( name, windowW, windowH )
1105 def textile_popup_help( name, windowW, windowH )
1103 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1106 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1104 end
1107 end
1105
1108
1106 # HTML cleansing stuff
1109 # HTML cleansing stuff
1107 BASIC_TAGS = {
1110 BASIC_TAGS = {
1108 'a' => ['href', 'title'],
1111 'a' => ['href', 'title'],
1109 'img' => ['src', 'alt', 'title'],
1112 'img' => ['src', 'alt', 'title'],
1110 'br' => [],
1113 'br' => [],
1111 'i' => nil,
1114 'i' => nil,
1112 'u' => nil,
1115 'u' => nil,
1113 'b' => nil,
1116 'b' => nil,
1114 'pre' => nil,
1117 'pre' => nil,
1115 'kbd' => nil,
1118 'kbd' => nil,
1116 'code' => ['lang'],
1119 'code' => ['lang'],
1117 'cite' => nil,
1120 'cite' => nil,
1118 'strong' => nil,
1121 'strong' => nil,
1119 'em' => nil,
1122 'em' => nil,
1120 'ins' => nil,
1123 'ins' => nil,
1121 'sup' => nil,
1124 'sup' => nil,
1122 'sub' => nil,
1125 'sub' => nil,
1123 'del' => nil,
1126 'del' => nil,
1124 'table' => nil,
1127 'table' => nil,
1125 'tr' => nil,
1128 'tr' => nil,
1126 'td' => ['colspan', 'rowspan'],
1129 'td' => ['colspan', 'rowspan'],
1127 'th' => nil,
1130 'th' => nil,
1128 'ol' => nil,
1131 'ol' => nil,
1129 'ul' => nil,
1132 'ul' => nil,
1130 'li' => nil,
1133 'li' => nil,
1131 'p' => nil,
1134 'p' => nil,
1132 'h1' => nil,
1135 'h1' => nil,
1133 'h2' => nil,
1136 'h2' => nil,
1134 'h3' => nil,
1137 'h3' => nil,
1135 'h4' => nil,
1138 'h4' => nil,
1136 'h5' => nil,
1139 'h5' => nil,
1137 'h6' => nil,
1140 'h6' => nil,
1138 'blockquote' => ['cite']
1141 'blockquote' => ['cite']
1139 }
1142 }
1140
1143
1141 def clean_html( text, tags = BASIC_TAGS )
1144 def clean_html( text, tags = BASIC_TAGS )
1142 text.gsub!( /<!\[CDATA\[/, '' )
1145 text.gsub!( /<!\[CDATA\[/, '' )
1143 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1146 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1144 raw = $~
1147 raw = $~
1145 tag = raw[2].downcase
1148 tag = raw[2].downcase
1146 if tags.has_key? tag
1149 if tags.has_key? tag
1147 pcs = [tag]
1150 pcs = [tag]
1148 tags[tag].each do |prop|
1151 tags[tag].each do |prop|
1149 ['"', "'", ''].each do |q|
1152 ['"', "'", ''].each do |q|
1150 q2 = ( q != '' ? q : '\s' )
1153 q2 = ( q != '' ? q : '\s' )
1151 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1154 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1152 attrv = $1
1155 attrv = $1
1153 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1156 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1154 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1157 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1155 break
1158 break
1156 end
1159 end
1157 end
1160 end
1158 end if tags[tag]
1161 end if tags[tag]
1159 "<#{raw[1]}#{pcs.join " "}>"
1162 "<#{raw[1]}#{pcs.join " "}>"
1160 else
1163 else
1161 " "
1164 " "
1162 end
1165 end
1163 end
1166 end
1164 end
1167 end
1165
1168
1166 ALLOWED_TAGS = %w(redpre pre code notextile)
1169 ALLOWED_TAGS = %w(redpre pre code notextile)
1167
1170
1168 def escape_html_tags(text)
1171 def escape_html_tags(text)
1169 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1172 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1170 end
1173 end
1171 end
1174 end
1172
1175
@@ -1,729 +1,739
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'issues_controller'
19 require 'issues_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
22 class IssuesController; def rescue_action(e) raise e end; end
23
23
24 class IssuesControllerTest < Test::Unit::TestCase
24 class IssuesControllerTest < Test::Unit::TestCase
25 fixtures :projects,
25 fixtures :projects,
26 :users,
26 :users,
27 :roles,
27 :roles,
28 :members,
28 :members,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :versions,
31 :versions,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :issue_categories,
34 :issue_categories,
35 :enabled_modules,
35 :enabled_modules,
36 :enumerations,
36 :enumerations,
37 :attachments,
37 :attachments,
38 :workflows,
38 :workflows,
39 :custom_fields,
39 :custom_fields,
40 :custom_values,
40 :custom_values,
41 :custom_fields_trackers,
41 :custom_fields_trackers,
42 :time_entries,
42 :time_entries,
43 :journals,
43 :journals,
44 :journal_details
44 :journal_details
45
45
46 def setup
46 def setup
47 @controller = IssuesController.new
47 @controller = IssuesController.new
48 @request = ActionController::TestRequest.new
48 @request = ActionController::TestRequest.new
49 @response = ActionController::TestResponse.new
49 @response = ActionController::TestResponse.new
50 User.current = nil
50 User.current = nil
51 end
51 end
52
52
53 def test_index
53 def test_index
54 get :index
54 get :index
55 assert_response :success
55 assert_response :success
56 assert_template 'index.rhtml'
56 assert_template 'index.rhtml'
57 assert_not_nil assigns(:issues)
57 assert_not_nil assigns(:issues)
58 assert_nil assigns(:project)
58 assert_nil assigns(:project)
59 assert_tag :tag => 'a', :content => /Can't print recipes/
59 assert_tag :tag => 'a', :content => /Can't print recipes/
60 assert_tag :tag => 'a', :content => /Subproject issue/
60 assert_tag :tag => 'a', :content => /Subproject issue/
61 # private projects hidden
61 # private projects hidden
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
62 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
63 assert_no_tag :tag => 'a', :content => /Issue on project 2/
64 end
64 end
65
65
66 def test_index_should_not_list_issues_when_module_disabled
66 def test_index_should_not_list_issues_when_module_disabled
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
67 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
68 get :index
68 get :index
69 assert_response :success
69 assert_response :success
70 assert_template 'index.rhtml'
70 assert_template 'index.rhtml'
71 assert_not_nil assigns(:issues)
71 assert_not_nil assigns(:issues)
72 assert_nil assigns(:project)
72 assert_nil assigns(:project)
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
73 assert_no_tag :tag => 'a', :content => /Can't print recipes/
74 assert_tag :tag => 'a', :content => /Subproject issue/
74 assert_tag :tag => 'a', :content => /Subproject issue/
75 end
75 end
76
76
77 def test_index_with_project
77 def test_index_with_project
78 Setting.display_subprojects_issues = 0
78 Setting.display_subprojects_issues = 0
79 get :index, :project_id => 1
79 get :index, :project_id => 1
80 assert_response :success
80 assert_response :success
81 assert_template 'index.rhtml'
81 assert_template 'index.rhtml'
82 assert_not_nil assigns(:issues)
82 assert_not_nil assigns(:issues)
83 assert_tag :tag => 'a', :content => /Can't print recipes/
83 assert_tag :tag => 'a', :content => /Can't print recipes/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
84 assert_no_tag :tag => 'a', :content => /Subproject issue/
85 end
85 end
86
86
87 def test_index_with_project_and_subprojects
87 def test_index_with_project_and_subprojects
88 Setting.display_subprojects_issues = 1
88 Setting.display_subprojects_issues = 1
89 get :index, :project_id => 1
89 get :index, :project_id => 1
90 assert_response :success
90 assert_response :success
91 assert_template 'index.rhtml'
91 assert_template 'index.rhtml'
92 assert_not_nil assigns(:issues)
92 assert_not_nil assigns(:issues)
93 assert_tag :tag => 'a', :content => /Can't print recipes/
93 assert_tag :tag => 'a', :content => /Can't print recipes/
94 assert_tag :tag => 'a', :content => /Subproject issue/
94 assert_tag :tag => 'a', :content => /Subproject issue/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
95 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
96 end
96 end
97
97
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
98 def test_index_with_project_and_subprojects_should_show_private_subprojects
99 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
100 Setting.display_subprojects_issues = 1
100 Setting.display_subprojects_issues = 1
101 get :index, :project_id => 1
101 get :index, :project_id => 1
102 assert_response :success
102 assert_response :success
103 assert_template 'index.rhtml'
103 assert_template 'index.rhtml'
104 assert_not_nil assigns(:issues)
104 assert_not_nil assigns(:issues)
105 assert_tag :tag => 'a', :content => /Can't print recipes/
105 assert_tag :tag => 'a', :content => /Can't print recipes/
106 assert_tag :tag => 'a', :content => /Subproject issue/
106 assert_tag :tag => 'a', :content => /Subproject issue/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
107 assert_tag :tag => 'a', :content => /Issue of a private subproject/
108 end
108 end
109
109
110 def test_index_with_project_and_filter
110 def test_index_with_project_and_filter
111 get :index, :project_id => 1, :set_filter => 1
111 get :index, :project_id => 1, :set_filter => 1
112 assert_response :success
112 assert_response :success
113 assert_template 'index.rhtml'
113 assert_template 'index.rhtml'
114 assert_not_nil assigns(:issues)
114 assert_not_nil assigns(:issues)
115 end
115 end
116
116
117 def test_index_csv_with_project
117 def test_index_csv_with_project
118 get :index, :format => 'csv'
118 get :index, :format => 'csv'
119 assert_response :success
119 assert_response :success
120 assert_not_nil assigns(:issues)
120 assert_not_nil assigns(:issues)
121 assert_equal 'text/csv', @response.content_type
121 assert_equal 'text/csv', @response.content_type
122
122
123 get :index, :project_id => 1, :format => 'csv'
123 get :index, :project_id => 1, :format => 'csv'
124 assert_response :success
124 assert_response :success
125 assert_not_nil assigns(:issues)
125 assert_not_nil assigns(:issues)
126 assert_equal 'text/csv', @response.content_type
126 assert_equal 'text/csv', @response.content_type
127 end
127 end
128
128
129 def test_index_pdf
129 def test_index_pdf
130 get :index, :format => 'pdf'
130 get :index, :format => 'pdf'
131 assert_response :success
131 assert_response :success
132 assert_not_nil assigns(:issues)
132 assert_not_nil assigns(:issues)
133 assert_equal 'application/pdf', @response.content_type
133 assert_equal 'application/pdf', @response.content_type
134
134
135 get :index, :project_id => 1, :format => 'pdf'
135 get :index, :project_id => 1, :format => 'pdf'
136 assert_response :success
136 assert_response :success
137 assert_not_nil assigns(:issues)
137 assert_not_nil assigns(:issues)
138 assert_equal 'application/pdf', @response.content_type
138 assert_equal 'application/pdf', @response.content_type
139 end
139 end
140
141 def test_index_sort
142 get :index, :sort_key => 'tracker'
143 assert_response :success
144
145 sort_params = @request.session['issuesindex_sort']
146 assert sort_params.is_a?(Hash)
147 assert_equal 'tracker', sort_params[:key]
148 assert_equal 'ASC', sort_params[:order]
149 end
140
150
141 def test_gantt
151 def test_gantt
142 get :gantt, :project_id => 1
152 get :gantt, :project_id => 1
143 assert_response :success
153 assert_response :success
144 assert_template 'gantt.rhtml'
154 assert_template 'gantt.rhtml'
145 assert_not_nil assigns(:gantt)
155 assert_not_nil assigns(:gantt)
146 events = assigns(:gantt).events
156 events = assigns(:gantt).events
147 assert_not_nil events
157 assert_not_nil events
148 # Issue with start and due dates
158 # Issue with start and due dates
149 i = Issue.find(1)
159 i = Issue.find(1)
150 assert_not_nil i.due_date
160 assert_not_nil i.due_date
151 assert events.include?(Issue.find(1))
161 assert events.include?(Issue.find(1))
152 # Issue with without due date but targeted to a version with date
162 # Issue with without due date but targeted to a version with date
153 i = Issue.find(2)
163 i = Issue.find(2)
154 assert_nil i.due_date
164 assert_nil i.due_date
155 assert events.include?(i)
165 assert events.include?(i)
156 end
166 end
157
167
158 def test_cross_project_gantt
168 def test_cross_project_gantt
159 get :gantt
169 get :gantt
160 assert_response :success
170 assert_response :success
161 assert_template 'gantt.rhtml'
171 assert_template 'gantt.rhtml'
162 assert_not_nil assigns(:gantt)
172 assert_not_nil assigns(:gantt)
163 events = assigns(:gantt).events
173 events = assigns(:gantt).events
164 assert_not_nil events
174 assert_not_nil events
165 end
175 end
166
176
167 def test_gantt_export_to_pdf
177 def test_gantt_export_to_pdf
168 get :gantt, :project_id => 1, :format => 'pdf'
178 get :gantt, :project_id => 1, :format => 'pdf'
169 assert_response :success
179 assert_response :success
170 assert_template 'gantt.rfpdf'
180 assert_template 'gantt.rfpdf'
171 assert_equal 'application/pdf', @response.content_type
181 assert_equal 'application/pdf', @response.content_type
172 assert_not_nil assigns(:gantt)
182 assert_not_nil assigns(:gantt)
173 end
183 end
174
184
175 def test_cross_project_gantt_export_to_pdf
185 def test_cross_project_gantt_export_to_pdf
176 get :gantt, :format => 'pdf'
186 get :gantt, :format => 'pdf'
177 assert_response :success
187 assert_response :success
178 assert_template 'gantt.rfpdf'
188 assert_template 'gantt.rfpdf'
179 assert_equal 'application/pdf', @response.content_type
189 assert_equal 'application/pdf', @response.content_type
180 assert_not_nil assigns(:gantt)
190 assert_not_nil assigns(:gantt)
181 end
191 end
182
192
183 if Object.const_defined?(:Magick)
193 if Object.const_defined?(:Magick)
184 def test_gantt_image
194 def test_gantt_image
185 get :gantt, :project_id => 1, :format => 'png'
195 get :gantt, :project_id => 1, :format => 'png'
186 assert_response :success
196 assert_response :success
187 assert_equal 'image/png', @response.content_type
197 assert_equal 'image/png', @response.content_type
188 end
198 end
189 else
199 else
190 puts "RMagick not installed. Skipping tests !!!"
200 puts "RMagick not installed. Skipping tests !!!"
191 end
201 end
192
202
193 def test_calendar
203 def test_calendar
194 get :calendar, :project_id => 1
204 get :calendar, :project_id => 1
195 assert_response :success
205 assert_response :success
196 assert_template 'calendar'
206 assert_template 'calendar'
197 assert_not_nil assigns(:calendar)
207 assert_not_nil assigns(:calendar)
198 end
208 end
199
209
200 def test_cross_project_calendar
210 def test_cross_project_calendar
201 get :calendar
211 get :calendar
202 assert_response :success
212 assert_response :success
203 assert_template 'calendar'
213 assert_template 'calendar'
204 assert_not_nil assigns(:calendar)
214 assert_not_nil assigns(:calendar)
205 end
215 end
206
216
207 def test_changes
217 def test_changes
208 get :changes, :project_id => 1
218 get :changes, :project_id => 1
209 assert_response :success
219 assert_response :success
210 assert_not_nil assigns(:journals)
220 assert_not_nil assigns(:journals)
211 assert_equal 'application/atom+xml', @response.content_type
221 assert_equal 'application/atom+xml', @response.content_type
212 end
222 end
213
223
214 def test_show_by_anonymous
224 def test_show_by_anonymous
215 get :show, :id => 1
225 get :show, :id => 1
216 assert_response :success
226 assert_response :success
217 assert_template 'show.rhtml'
227 assert_template 'show.rhtml'
218 assert_not_nil assigns(:issue)
228 assert_not_nil assigns(:issue)
219 assert_equal Issue.find(1), assigns(:issue)
229 assert_equal Issue.find(1), assigns(:issue)
220
230
221 # anonymous role is allowed to add a note
231 # anonymous role is allowed to add a note
222 assert_tag :tag => 'form',
232 assert_tag :tag => 'form',
223 :descendant => { :tag => 'fieldset',
233 :descendant => { :tag => 'fieldset',
224 :child => { :tag => 'legend',
234 :child => { :tag => 'legend',
225 :content => /Notes/ } }
235 :content => /Notes/ } }
226 end
236 end
227
237
228 def test_show_by_manager
238 def test_show_by_manager
229 @request.session[:user_id] = 2
239 @request.session[:user_id] = 2
230 get :show, :id => 1
240 get :show, :id => 1
231 assert_response :success
241 assert_response :success
232
242
233 assert_tag :tag => 'form',
243 assert_tag :tag => 'form',
234 :descendant => { :tag => 'fieldset',
244 :descendant => { :tag => 'fieldset',
235 :child => { :tag => 'legend',
245 :child => { :tag => 'legend',
236 :content => /Change properties/ } },
246 :content => /Change properties/ } },
237 :descendant => { :tag => 'fieldset',
247 :descendant => { :tag => 'fieldset',
238 :child => { :tag => 'legend',
248 :child => { :tag => 'legend',
239 :content => /Log time/ } },
249 :content => /Log time/ } },
240 :descendant => { :tag => 'fieldset',
250 :descendant => { :tag => 'fieldset',
241 :child => { :tag => 'legend',
251 :child => { :tag => 'legend',
242 :content => /Notes/ } }
252 :content => /Notes/ } }
243 end
253 end
244
254
245 def test_get_new
255 def test_get_new
246 @request.session[:user_id] = 2
256 @request.session[:user_id] = 2
247 get :new, :project_id => 1, :tracker_id => 1
257 get :new, :project_id => 1, :tracker_id => 1
248 assert_response :success
258 assert_response :success
249 assert_template 'new'
259 assert_template 'new'
250
260
251 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
261 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
252 :value => 'Default string' }
262 :value => 'Default string' }
253 end
263 end
254
264
255 def test_get_new_without_tracker_id
265 def test_get_new_without_tracker_id
256 @request.session[:user_id] = 2
266 @request.session[:user_id] = 2
257 get :new, :project_id => 1
267 get :new, :project_id => 1
258 assert_response :success
268 assert_response :success
259 assert_template 'new'
269 assert_template 'new'
260
270
261 issue = assigns(:issue)
271 issue = assigns(:issue)
262 assert_not_nil issue
272 assert_not_nil issue
263 assert_equal Project.find(1).trackers.first, issue.tracker
273 assert_equal Project.find(1).trackers.first, issue.tracker
264 end
274 end
265
275
266 def test_update_new_form
276 def test_update_new_form
267 @request.session[:user_id] = 2
277 @request.session[:user_id] = 2
268 xhr :post, :new, :project_id => 1,
278 xhr :post, :new, :project_id => 1,
269 :issue => {:tracker_id => 2,
279 :issue => {:tracker_id => 2,
270 :subject => 'This is the test_new issue',
280 :subject => 'This is the test_new issue',
271 :description => 'This is the description',
281 :description => 'This is the description',
272 :priority_id => 5}
282 :priority_id => 5}
273 assert_response :success
283 assert_response :success
274 assert_template 'new'
284 assert_template 'new'
275 end
285 end
276
286
277 def test_post_new
287 def test_post_new
278 @request.session[:user_id] = 2
288 @request.session[:user_id] = 2
279 post :new, :project_id => 1,
289 post :new, :project_id => 1,
280 :issue => {:tracker_id => 3,
290 :issue => {:tracker_id => 3,
281 :subject => 'This is the test_new issue',
291 :subject => 'This is the test_new issue',
282 :description => 'This is the description',
292 :description => 'This is the description',
283 :priority_id => 5,
293 :priority_id => 5,
284 :estimated_hours => '',
294 :estimated_hours => '',
285 :custom_field_values => {'2' => 'Value for field 2'}}
295 :custom_field_values => {'2' => 'Value for field 2'}}
286 assert_redirected_to 'issues/show'
296 assert_redirected_to 'issues/show'
287
297
288 issue = Issue.find_by_subject('This is the test_new issue')
298 issue = Issue.find_by_subject('This is the test_new issue')
289 assert_not_nil issue
299 assert_not_nil issue
290 assert_equal 2, issue.author_id
300 assert_equal 2, issue.author_id
291 assert_equal 3, issue.tracker_id
301 assert_equal 3, issue.tracker_id
292 assert_nil issue.estimated_hours
302 assert_nil issue.estimated_hours
293 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
303 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
294 assert_not_nil v
304 assert_not_nil v
295 assert_equal 'Value for field 2', v.value
305 assert_equal 'Value for field 2', v.value
296 end
306 end
297
307
298 def test_post_new_without_custom_fields_param
308 def test_post_new_without_custom_fields_param
299 @request.session[:user_id] = 2
309 @request.session[:user_id] = 2
300 post :new, :project_id => 1,
310 post :new, :project_id => 1,
301 :issue => {:tracker_id => 1,
311 :issue => {:tracker_id => 1,
302 :subject => 'This is the test_new issue',
312 :subject => 'This is the test_new issue',
303 :description => 'This is the description',
313 :description => 'This is the description',
304 :priority_id => 5}
314 :priority_id => 5}
305 assert_redirected_to 'issues/show'
315 assert_redirected_to 'issues/show'
306 end
316 end
307
317
308 def test_post_new_with_required_custom_field_and_without_custom_fields_param
318 def test_post_new_with_required_custom_field_and_without_custom_fields_param
309 field = IssueCustomField.find_by_name('Database')
319 field = IssueCustomField.find_by_name('Database')
310 field.update_attribute(:is_required, true)
320 field.update_attribute(:is_required, true)
311
321
312 @request.session[:user_id] = 2
322 @request.session[:user_id] = 2
313 post :new, :project_id => 1,
323 post :new, :project_id => 1,
314 :issue => {:tracker_id => 1,
324 :issue => {:tracker_id => 1,
315 :subject => 'This is the test_new issue',
325 :subject => 'This is the test_new issue',
316 :description => 'This is the description',
326 :description => 'This is the description',
317 :priority_id => 5}
327 :priority_id => 5}
318 assert_response :success
328 assert_response :success
319 assert_template 'new'
329 assert_template 'new'
320 issue = assigns(:issue)
330 issue = assigns(:issue)
321 assert_not_nil issue
331 assert_not_nil issue
322 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
332 assert_equal 'activerecord_error_invalid', issue.errors.on(:custom_values)
323 end
333 end
324
334
325 def test_post_should_preserve_fields_values_on_validation_failure
335 def test_post_should_preserve_fields_values_on_validation_failure
326 @request.session[:user_id] = 2
336 @request.session[:user_id] = 2
327 post :new, :project_id => 1,
337 post :new, :project_id => 1,
328 :issue => {:tracker_id => 1,
338 :issue => {:tracker_id => 1,
329 :subject => 'This is the test_new issue',
339 :subject => 'This is the test_new issue',
330 # empty description
340 # empty description
331 :description => '',
341 :description => '',
332 :priority_id => 6,
342 :priority_id => 6,
333 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
343 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
334 assert_response :success
344 assert_response :success
335 assert_template 'new'
345 assert_template 'new'
336
346
337 assert_tag :input, :attributes => { :name => 'issue[subject]',
347 assert_tag :input, :attributes => { :name => 'issue[subject]',
338 :value => 'This is the test_new issue' }
348 :value => 'This is the test_new issue' }
339 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
349 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
340 :child => { :tag => 'option', :attributes => { :selected => 'selected',
350 :child => { :tag => 'option', :attributes => { :selected => 'selected',
341 :value => '6' },
351 :value => '6' },
342 :content => 'High' }
352 :content => 'High' }
343 # Custom fields
353 # Custom fields
344 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
354 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
345 :child => { :tag => 'option', :attributes => { :selected => 'selected',
355 :child => { :tag => 'option', :attributes => { :selected => 'selected',
346 :value => 'Oracle' },
356 :value => 'Oracle' },
347 :content => 'Oracle' }
357 :content => 'Oracle' }
348 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
358 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
349 :value => 'Value for field 2'}
359 :value => 'Value for field 2'}
350 end
360 end
351
361
352 def test_copy_issue
362 def test_copy_issue
353 @request.session[:user_id] = 2
363 @request.session[:user_id] = 2
354 get :new, :project_id => 1, :copy_from => 1
364 get :new, :project_id => 1, :copy_from => 1
355 assert_template 'new'
365 assert_template 'new'
356 assert_not_nil assigns(:issue)
366 assert_not_nil assigns(:issue)
357 orig = Issue.find(1)
367 orig = Issue.find(1)
358 assert_equal orig.subject, assigns(:issue).subject
368 assert_equal orig.subject, assigns(:issue).subject
359 end
369 end
360
370
361 def test_get_edit
371 def test_get_edit
362 @request.session[:user_id] = 2
372 @request.session[:user_id] = 2
363 get :edit, :id => 1
373 get :edit, :id => 1
364 assert_response :success
374 assert_response :success
365 assert_template 'edit'
375 assert_template 'edit'
366 assert_not_nil assigns(:issue)
376 assert_not_nil assigns(:issue)
367 assert_equal Issue.find(1), assigns(:issue)
377 assert_equal Issue.find(1), assigns(:issue)
368 end
378 end
369
379
370 def test_get_edit_with_params
380 def test_get_edit_with_params
371 @request.session[:user_id] = 2
381 @request.session[:user_id] = 2
372 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
382 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
373 assert_response :success
383 assert_response :success
374 assert_template 'edit'
384 assert_template 'edit'
375
385
376 issue = assigns(:issue)
386 issue = assigns(:issue)
377 assert_not_nil issue
387 assert_not_nil issue
378
388
379 assert_equal 5, issue.status_id
389 assert_equal 5, issue.status_id
380 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
390 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
381 :child => { :tag => 'option',
391 :child => { :tag => 'option',
382 :content => 'Closed',
392 :content => 'Closed',
383 :attributes => { :selected => 'selected' } }
393 :attributes => { :selected => 'selected' } }
384
394
385 assert_equal 7, issue.priority_id
395 assert_equal 7, issue.priority_id
386 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
396 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
387 :child => { :tag => 'option',
397 :child => { :tag => 'option',
388 :content => 'Urgent',
398 :content => 'Urgent',
389 :attributes => { :selected => 'selected' } }
399 :attributes => { :selected => 'selected' } }
390 end
400 end
391
401
392 def test_reply_to_issue
402 def test_reply_to_issue
393 @request.session[:user_id] = 2
403 @request.session[:user_id] = 2
394 get :reply, :id => 1
404 get :reply, :id => 1
395 assert_response :success
405 assert_response :success
396 assert_select_rjs :show, "update"
406 assert_select_rjs :show, "update"
397 end
407 end
398
408
399 def test_reply_to_note
409 def test_reply_to_note
400 @request.session[:user_id] = 2
410 @request.session[:user_id] = 2
401 get :reply, :id => 1, :journal_id => 2
411 get :reply, :id => 1, :journal_id => 2
402 assert_response :success
412 assert_response :success
403 assert_select_rjs :show, "update"
413 assert_select_rjs :show, "update"
404 end
414 end
405
415
406 def test_post_edit_without_custom_fields_param
416 def test_post_edit_without_custom_fields_param
407 @request.session[:user_id] = 2
417 @request.session[:user_id] = 2
408 ActionMailer::Base.deliveries.clear
418 ActionMailer::Base.deliveries.clear
409
419
410 issue = Issue.find(1)
420 issue = Issue.find(1)
411 assert_equal '125', issue.custom_value_for(2).value
421 assert_equal '125', issue.custom_value_for(2).value
412 old_subject = issue.subject
422 old_subject = issue.subject
413 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
423 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
414
424
415 assert_difference('Journal.count') do
425 assert_difference('Journal.count') do
416 assert_difference('JournalDetail.count', 2) do
426 assert_difference('JournalDetail.count', 2) do
417 post :edit, :id => 1, :issue => {:subject => new_subject,
427 post :edit, :id => 1, :issue => {:subject => new_subject,
418 :priority_id => '6',
428 :priority_id => '6',
419 :category_id => '1' # no change
429 :category_id => '1' # no change
420 }
430 }
421 end
431 end
422 end
432 end
423 assert_redirected_to 'issues/show/1'
433 assert_redirected_to 'issues/show/1'
424 issue.reload
434 issue.reload
425 assert_equal new_subject, issue.subject
435 assert_equal new_subject, issue.subject
426 # Make sure custom fields were not cleared
436 # Make sure custom fields were not cleared
427 assert_equal '125', issue.custom_value_for(2).value
437 assert_equal '125', issue.custom_value_for(2).value
428
438
429 mail = ActionMailer::Base.deliveries.last
439 mail = ActionMailer::Base.deliveries.last
430 assert_kind_of TMail::Mail, mail
440 assert_kind_of TMail::Mail, mail
431 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
441 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
432 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
442 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
433 end
443 end
434
444
435 def test_post_edit_with_custom_field_change
445 def test_post_edit_with_custom_field_change
436 @request.session[:user_id] = 2
446 @request.session[:user_id] = 2
437 issue = Issue.find(1)
447 issue = Issue.find(1)
438 assert_equal '125', issue.custom_value_for(2).value
448 assert_equal '125', issue.custom_value_for(2).value
439
449
440 assert_difference('Journal.count') do
450 assert_difference('Journal.count') do
441 assert_difference('JournalDetail.count', 3) do
451 assert_difference('JournalDetail.count', 3) do
442 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
452 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
443 :priority_id => '6',
453 :priority_id => '6',
444 :category_id => '1', # no change
454 :category_id => '1', # no change
445 :custom_field_values => { '2' => 'New custom value' }
455 :custom_field_values => { '2' => 'New custom value' }
446 }
456 }
447 end
457 end
448 end
458 end
449 assert_redirected_to 'issues/show/1'
459 assert_redirected_to 'issues/show/1'
450 issue.reload
460 issue.reload
451 assert_equal 'New custom value', issue.custom_value_for(2).value
461 assert_equal 'New custom value', issue.custom_value_for(2).value
452
462
453 mail = ActionMailer::Base.deliveries.last
463 mail = ActionMailer::Base.deliveries.last
454 assert_kind_of TMail::Mail, mail
464 assert_kind_of TMail::Mail, mail
455 assert mail.body.include?("Searchable field changed from 125 to New custom value")
465 assert mail.body.include?("Searchable field changed from 125 to New custom value")
456 end
466 end
457
467
458 def test_post_edit_with_status_and_assignee_change
468 def test_post_edit_with_status_and_assignee_change
459 issue = Issue.find(1)
469 issue = Issue.find(1)
460 assert_equal 1, issue.status_id
470 assert_equal 1, issue.status_id
461 @request.session[:user_id] = 2
471 @request.session[:user_id] = 2
462 assert_difference('TimeEntry.count', 0) do
472 assert_difference('TimeEntry.count', 0) do
463 post :edit,
473 post :edit,
464 :id => 1,
474 :id => 1,
465 :issue => { :status_id => 2, :assigned_to_id => 3 },
475 :issue => { :status_id => 2, :assigned_to_id => 3 },
466 :notes => 'Assigned to dlopper',
476 :notes => 'Assigned to dlopper',
467 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
477 :time_entry => { :hours => '', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
468 end
478 end
469 assert_redirected_to 'issues/show/1'
479 assert_redirected_to 'issues/show/1'
470 issue.reload
480 issue.reload
471 assert_equal 2, issue.status_id
481 assert_equal 2, issue.status_id
472 j = issue.journals.find(:first, :order => 'id DESC')
482 j = issue.journals.find(:first, :order => 'id DESC')
473 assert_equal 'Assigned to dlopper', j.notes
483 assert_equal 'Assigned to dlopper', j.notes
474 assert_equal 2, j.details.size
484 assert_equal 2, j.details.size
475
485
476 mail = ActionMailer::Base.deliveries.last
486 mail = ActionMailer::Base.deliveries.last
477 assert mail.body.include?("Status changed from New to Assigned")
487 assert mail.body.include?("Status changed from New to Assigned")
478 end
488 end
479
489
480 def test_post_edit_with_note_only
490 def test_post_edit_with_note_only
481 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
491 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
482 # anonymous user
492 # anonymous user
483 post :edit,
493 post :edit,
484 :id => 1,
494 :id => 1,
485 :notes => notes
495 :notes => notes
486 assert_redirected_to 'issues/show/1'
496 assert_redirected_to 'issues/show/1'
487 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
497 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
488 assert_equal notes, j.notes
498 assert_equal notes, j.notes
489 assert_equal 0, j.details.size
499 assert_equal 0, j.details.size
490 assert_equal User.anonymous, j.user
500 assert_equal User.anonymous, j.user
491
501
492 mail = ActionMailer::Base.deliveries.last
502 mail = ActionMailer::Base.deliveries.last
493 assert mail.body.include?(notes)
503 assert mail.body.include?(notes)
494 end
504 end
495
505
496 def test_post_edit_with_note_and_spent_time
506 def test_post_edit_with_note_and_spent_time
497 @request.session[:user_id] = 2
507 @request.session[:user_id] = 2
498 spent_hours_before = Issue.find(1).spent_hours
508 spent_hours_before = Issue.find(1).spent_hours
499 assert_difference('TimeEntry.count') do
509 assert_difference('TimeEntry.count') do
500 post :edit,
510 post :edit,
501 :id => 1,
511 :id => 1,
502 :notes => '2.5 hours added',
512 :notes => '2.5 hours added',
503 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
513 :time_entry => { :hours => '2.5', :comments => '', :activity_id => Enumeration.get_values('ACTI').first }
504 end
514 end
505 assert_redirected_to 'issues/show/1'
515 assert_redirected_to 'issues/show/1'
506
516
507 issue = Issue.find(1)
517 issue = Issue.find(1)
508
518
509 j = issue.journals.find(:first, :order => 'id DESC')
519 j = issue.journals.find(:first, :order => 'id DESC')
510 assert_equal '2.5 hours added', j.notes
520 assert_equal '2.5 hours added', j.notes
511 assert_equal 0, j.details.size
521 assert_equal 0, j.details.size
512
522
513 t = issue.time_entries.find(:first, :order => 'id DESC')
523 t = issue.time_entries.find(:first, :order => 'id DESC')
514 assert_not_nil t
524 assert_not_nil t
515 assert_equal 2.5, t.hours
525 assert_equal 2.5, t.hours
516 assert_equal spent_hours_before + 2.5, issue.spent_hours
526 assert_equal spent_hours_before + 2.5, issue.spent_hours
517 end
527 end
518
528
519 def test_post_edit_with_attachment_only
529 def test_post_edit_with_attachment_only
520 set_tmp_attachments_directory
530 set_tmp_attachments_directory
521
531
522 # Delete all fixtured journals, a race condition can occur causing the wrong
532 # Delete all fixtured journals, a race condition can occur causing the wrong
523 # journal to get fetched in the next find.
533 # journal to get fetched in the next find.
524 Journal.delete_all
534 Journal.delete_all
525
535
526 # anonymous user
536 # anonymous user
527 post :edit,
537 post :edit,
528 :id => 1,
538 :id => 1,
529 :notes => '',
539 :notes => '',
530 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
540 :attachments => {'1' => {'file' => test_uploaded_file('testfile.txt', 'text/plain')}}
531 assert_redirected_to 'issues/show/1'
541 assert_redirected_to 'issues/show/1'
532 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
542 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
533 assert j.notes.blank?
543 assert j.notes.blank?
534 assert_equal 1, j.details.size
544 assert_equal 1, j.details.size
535 assert_equal 'testfile.txt', j.details.first.value
545 assert_equal 'testfile.txt', j.details.first.value
536 assert_equal User.anonymous, j.user
546 assert_equal User.anonymous, j.user
537
547
538 mail = ActionMailer::Base.deliveries.last
548 mail = ActionMailer::Base.deliveries.last
539 assert mail.body.include?('testfile.txt')
549 assert mail.body.include?('testfile.txt')
540 end
550 end
541
551
542 def test_post_edit_with_no_change
552 def test_post_edit_with_no_change
543 issue = Issue.find(1)
553 issue = Issue.find(1)
544 issue.journals.clear
554 issue.journals.clear
545 ActionMailer::Base.deliveries.clear
555 ActionMailer::Base.deliveries.clear
546
556
547 post :edit,
557 post :edit,
548 :id => 1,
558 :id => 1,
549 :notes => ''
559 :notes => ''
550 assert_redirected_to 'issues/show/1'
560 assert_redirected_to 'issues/show/1'
551
561
552 issue.reload
562 issue.reload
553 assert issue.journals.empty?
563 assert issue.journals.empty?
554 # No email should be sent
564 # No email should be sent
555 assert ActionMailer::Base.deliveries.empty?
565 assert ActionMailer::Base.deliveries.empty?
556 end
566 end
557
567
558 def test_bulk_edit
568 def test_bulk_edit
559 @request.session[:user_id] = 2
569 @request.session[:user_id] = 2
560 # update issues priority
570 # update issues priority
561 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
571 post :bulk_edit, :ids => [1, 2], :priority_id => 7, :notes => 'Bulk editing', :assigned_to_id => ''
562 assert_response 302
572 assert_response 302
563 # check that the issues were updated
573 # check that the issues were updated
564 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
574 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
565 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
575 assert_equal 'Bulk editing', Issue.find(1).journals.find(:first, :order => 'created_on DESC').notes
566 end
576 end
567
577
568 def test_bulk_unassign
578 def test_bulk_unassign
569 assert_not_nil Issue.find(2).assigned_to
579 assert_not_nil Issue.find(2).assigned_to
570 @request.session[:user_id] = 2
580 @request.session[:user_id] = 2
571 # unassign issues
581 # unassign issues
572 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
582 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
573 assert_response 302
583 assert_response 302
574 # check that the issues were updated
584 # check that the issues were updated
575 assert_nil Issue.find(2).assigned_to
585 assert_nil Issue.find(2).assigned_to
576 end
586 end
577
587
578 def test_move_one_issue_to_another_project
588 def test_move_one_issue_to_another_project
579 @request.session[:user_id] = 1
589 @request.session[:user_id] = 1
580 post :move, :id => 1, :new_project_id => 2
590 post :move, :id => 1, :new_project_id => 2
581 assert_redirected_to 'projects/ecookbook/issues'
591 assert_redirected_to 'projects/ecookbook/issues'
582 assert_equal 2, Issue.find(1).project_id
592 assert_equal 2, Issue.find(1).project_id
583 end
593 end
584
594
585 def test_bulk_move_to_another_project
595 def test_bulk_move_to_another_project
586 @request.session[:user_id] = 1
596 @request.session[:user_id] = 1
587 post :move, :ids => [1, 2], :new_project_id => 2
597 post :move, :ids => [1, 2], :new_project_id => 2
588 assert_redirected_to 'projects/ecookbook/issues'
598 assert_redirected_to 'projects/ecookbook/issues'
589 # Issues moved to project 2
599 # Issues moved to project 2
590 assert_equal 2, Issue.find(1).project_id
600 assert_equal 2, Issue.find(1).project_id
591 assert_equal 2, Issue.find(2).project_id
601 assert_equal 2, Issue.find(2).project_id
592 # No tracker change
602 # No tracker change
593 assert_equal 1, Issue.find(1).tracker_id
603 assert_equal 1, Issue.find(1).tracker_id
594 assert_equal 2, Issue.find(2).tracker_id
604 assert_equal 2, Issue.find(2).tracker_id
595 end
605 end
596
606
597 def test_bulk_move_to_another_tracker
607 def test_bulk_move_to_another_tracker
598 @request.session[:user_id] = 1
608 @request.session[:user_id] = 1
599 post :move, :ids => [1, 2], :new_tracker_id => 2
609 post :move, :ids => [1, 2], :new_tracker_id => 2
600 assert_redirected_to 'projects/ecookbook/issues'
610 assert_redirected_to 'projects/ecookbook/issues'
601 assert_equal 2, Issue.find(1).tracker_id
611 assert_equal 2, Issue.find(1).tracker_id
602 assert_equal 2, Issue.find(2).tracker_id
612 assert_equal 2, Issue.find(2).tracker_id
603 end
613 end
604
614
605 def test_context_menu_one_issue
615 def test_context_menu_one_issue
606 @request.session[:user_id] = 2
616 @request.session[:user_id] = 2
607 get :context_menu, :ids => [1]
617 get :context_menu, :ids => [1]
608 assert_response :success
618 assert_response :success
609 assert_template 'context_menu'
619 assert_template 'context_menu'
610 assert_tag :tag => 'a', :content => 'Edit',
620 assert_tag :tag => 'a', :content => 'Edit',
611 :attributes => { :href => '/issues/edit/1',
621 :attributes => { :href => '/issues/edit/1',
612 :class => 'icon-edit' }
622 :class => 'icon-edit' }
613 assert_tag :tag => 'a', :content => 'Closed',
623 assert_tag :tag => 'a', :content => 'Closed',
614 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
624 :attributes => { :href => '/issues/edit/1?issue%5Bstatus_id%5D=5',
615 :class => '' }
625 :class => '' }
616 assert_tag :tag => 'a', :content => 'Immediate',
626 assert_tag :tag => 'a', :content => 'Immediate',
617 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
627 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
618 :class => '' }
628 :class => '' }
619 assert_tag :tag => 'a', :content => 'Dave Lopper',
629 assert_tag :tag => 'a', :content => 'Dave Lopper',
620 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
630 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
621 :class => '' }
631 :class => '' }
622 assert_tag :tag => 'a', :content => 'Copy',
632 assert_tag :tag => 'a', :content => 'Copy',
623 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
633 :attributes => { :href => '/projects/ecookbook/issues/new?copy_from=1',
624 :class => 'icon-copy' }
634 :class => 'icon-copy' }
625 assert_tag :tag => 'a', :content => 'Move',
635 assert_tag :tag => 'a', :content => 'Move',
626 :attributes => { :href => '/issues/move?ids%5B%5D=1',
636 :attributes => { :href => '/issues/move?ids%5B%5D=1',
627 :class => 'icon-move' }
637 :class => 'icon-move' }
628 assert_tag :tag => 'a', :content => 'Delete',
638 assert_tag :tag => 'a', :content => 'Delete',
629 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
639 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
630 :class => 'icon-del' }
640 :class => 'icon-del' }
631 end
641 end
632
642
633 def test_context_menu_one_issue_by_anonymous
643 def test_context_menu_one_issue_by_anonymous
634 get :context_menu, :ids => [1]
644 get :context_menu, :ids => [1]
635 assert_response :success
645 assert_response :success
636 assert_template 'context_menu'
646 assert_template 'context_menu'
637 assert_tag :tag => 'a', :content => 'Delete',
647 assert_tag :tag => 'a', :content => 'Delete',
638 :attributes => { :href => '#',
648 :attributes => { :href => '#',
639 :class => 'icon-del disabled' }
649 :class => 'icon-del disabled' }
640 end
650 end
641
651
642 def test_context_menu_multiple_issues_of_same_project
652 def test_context_menu_multiple_issues_of_same_project
643 @request.session[:user_id] = 2
653 @request.session[:user_id] = 2
644 get :context_menu, :ids => [1, 2]
654 get :context_menu, :ids => [1, 2]
645 assert_response :success
655 assert_response :success
646 assert_template 'context_menu'
656 assert_template 'context_menu'
647 assert_tag :tag => 'a', :content => 'Edit',
657 assert_tag :tag => 'a', :content => 'Edit',
648 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
658 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
649 :class => 'icon-edit' }
659 :class => 'icon-edit' }
650 assert_tag :tag => 'a', :content => 'Immediate',
660 assert_tag :tag => 'a', :content => 'Immediate',
651 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
661 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
652 :class => '' }
662 :class => '' }
653 assert_tag :tag => 'a', :content => 'Dave Lopper',
663 assert_tag :tag => 'a', :content => 'Dave Lopper',
654 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
664 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
655 :class => '' }
665 :class => '' }
656 assert_tag :tag => 'a', :content => 'Move',
666 assert_tag :tag => 'a', :content => 'Move',
657 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
667 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
658 :class => 'icon-move' }
668 :class => 'icon-move' }
659 assert_tag :tag => 'a', :content => 'Delete',
669 assert_tag :tag => 'a', :content => 'Delete',
660 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
670 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
661 :class => 'icon-del' }
671 :class => 'icon-del' }
662 end
672 end
663
673
664 def test_context_menu_multiple_issues_of_different_project
674 def test_context_menu_multiple_issues_of_different_project
665 @request.session[:user_id] = 2
675 @request.session[:user_id] = 2
666 get :context_menu, :ids => [1, 2, 4]
676 get :context_menu, :ids => [1, 2, 4]
667 assert_response :success
677 assert_response :success
668 assert_template 'context_menu'
678 assert_template 'context_menu'
669 assert_tag :tag => 'a', :content => 'Delete',
679 assert_tag :tag => 'a', :content => 'Delete',
670 :attributes => { :href => '#',
680 :attributes => { :href => '#',
671 :class => 'icon-del disabled' }
681 :class => 'icon-del disabled' }
672 end
682 end
673
683
674 def test_destroy_issue_with_no_time_entries
684 def test_destroy_issue_with_no_time_entries
675 assert_nil TimeEntry.find_by_issue_id(2)
685 assert_nil TimeEntry.find_by_issue_id(2)
676 @request.session[:user_id] = 2
686 @request.session[:user_id] = 2
677 post :destroy, :id => 2
687 post :destroy, :id => 2
678 assert_redirected_to 'projects/ecookbook/issues'
688 assert_redirected_to 'projects/ecookbook/issues'
679 assert_nil Issue.find_by_id(2)
689 assert_nil Issue.find_by_id(2)
680 end
690 end
681
691
682 def test_destroy_issues_with_time_entries
692 def test_destroy_issues_with_time_entries
683 @request.session[:user_id] = 2
693 @request.session[:user_id] = 2
684 post :destroy, :ids => [1, 3]
694 post :destroy, :ids => [1, 3]
685 assert_response :success
695 assert_response :success
686 assert_template 'destroy'
696 assert_template 'destroy'
687 assert_not_nil assigns(:hours)
697 assert_not_nil assigns(:hours)
688 assert Issue.find_by_id(1) && Issue.find_by_id(3)
698 assert Issue.find_by_id(1) && Issue.find_by_id(3)
689 end
699 end
690
700
691 def test_destroy_issues_and_destroy_time_entries
701 def test_destroy_issues_and_destroy_time_entries
692 @request.session[:user_id] = 2
702 @request.session[:user_id] = 2
693 post :destroy, :ids => [1, 3], :todo => 'destroy'
703 post :destroy, :ids => [1, 3], :todo => 'destroy'
694 assert_redirected_to 'projects/ecookbook/issues'
704 assert_redirected_to 'projects/ecookbook/issues'
695 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
705 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
696 assert_nil TimeEntry.find_by_id([1, 2])
706 assert_nil TimeEntry.find_by_id([1, 2])
697 end
707 end
698
708
699 def test_destroy_issues_and_assign_time_entries_to_project
709 def test_destroy_issues_and_assign_time_entries_to_project
700 @request.session[:user_id] = 2
710 @request.session[:user_id] = 2
701 post :destroy, :ids => [1, 3], :todo => 'nullify'
711 post :destroy, :ids => [1, 3], :todo => 'nullify'
702 assert_redirected_to 'projects/ecookbook/issues'
712 assert_redirected_to 'projects/ecookbook/issues'
703 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
713 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
704 assert_nil TimeEntry.find(1).issue_id
714 assert_nil TimeEntry.find(1).issue_id
705 assert_nil TimeEntry.find(2).issue_id
715 assert_nil TimeEntry.find(2).issue_id
706 end
716 end
707
717
708 def test_destroy_issues_and_reassign_time_entries_to_another_issue
718 def test_destroy_issues_and_reassign_time_entries_to_another_issue
709 @request.session[:user_id] = 2
719 @request.session[:user_id] = 2
710 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
720 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
711 assert_redirected_to 'projects/ecookbook/issues'
721 assert_redirected_to 'projects/ecookbook/issues'
712 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
722 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
713 assert_equal 2, TimeEntry.find(1).issue_id
723 assert_equal 2, TimeEntry.find(1).issue_id
714 assert_equal 2, TimeEntry.find(2).issue_id
724 assert_equal 2, TimeEntry.find(2).issue_id
715 end
725 end
716
726
717 def test_destroy_attachment
727 def test_destroy_attachment
718 issue = Issue.find(3)
728 issue = Issue.find(3)
719 a = issue.attachments.size
729 a = issue.attachments.size
720 @request.session[:user_id] = 2
730 @request.session[:user_id] = 2
721 post :destroy_attachment, :id => 3, :attachment_id => 1
731 post :destroy_attachment, :id => 3, :attachment_id => 1
722 assert_redirected_to 'issues/show/3'
732 assert_redirected_to 'issues/show/3'
723 assert_nil Attachment.find_by_id(1)
733 assert_nil Attachment.find_by_id(1)
724 issue.reload
734 issue.reload
725 assert_equal((a-1), issue.attachments.size)
735 assert_equal((a-1), issue.attachments.size)
726 j = issue.journals.find(:first, :order => 'created_on DESC')
736 j = issue.journals.find(:first, :order => 'created_on DESC')
727 assert_equal 'attachment', j.details.first.property
737 assert_equal 'attachment', j.details.first.property
728 end
738 end
729 end
739 end
@@ -1,433 +1,443
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.dirname(__FILE__) + '/../../test_helper'
18 require File.dirname(__FILE__) + '/../../test_helper'
19
19
20 class ApplicationHelperTest < HelperTestCase
20 class ApplicationHelperTest < HelperTestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 fixtures :projects, :roles, :enabled_modules, :users,
23 fixtures :projects, :roles, :enabled_modules, :users,
24 :repositories, :changesets,
24 :repositories, :changesets,
25 :trackers, :issue_statuses, :issues, :versions, :documents,
25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 :wikis, :wiki_pages, :wiki_contents,
26 :wikis, :wiki_pages, :wiki_contents,
27 :boards, :messages,
27 :boards, :messages,
28 :attachments
28 :attachments
29
29
30 def setup
30 def setup
31 super
31 super
32 end
32 end
33
33
34 def test_auto_links
34 def test_auto_links
35 to_test = {
35 to_test = {
36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
42 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
44 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
45 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
45 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
46 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
46 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
51 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 }
58 }
59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
60 end
60 end
61
61
62 def test_auto_mailto
62 def test_auto_mailto
63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
64 textilizable('test@foo.bar')
64 textilizable('test@foo.bar')
65 end
65 end
66
66
67 def test_inline_images
67 def test_inline_images
68 to_test = {
68 to_test = {
69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
73 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
73 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
74 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
74 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
75 }
75 }
76 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
76 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
77 end
77 end
78
78
79 def test_acronyms
80 to_test = {
81 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
82 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
83 }
84 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
85
86 end
87
79 def test_attached_images
88 def test_attached_images
80 to_test = {
89 to_test = {
81 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
90 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
82 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
91 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
83 }
92 }
84 attachments = Attachment.find(:all)
93 attachments = Attachment.find(:all)
85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
94 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
86 end
95 end
87
96
88 def test_textile_external_links
97 def test_textile_external_links
89 to_test = {
98 to_test = {
90 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
99 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
91 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
100 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
92 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
101 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
102 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
93 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
103 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
94 # no multiline link text
104 # no multiline link text
95 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
105 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
96 }
106 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
107 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
108 end
99
109
100 def test_redmine_links
110 def test_redmine_links
101 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
111 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
102 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
112 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
103
113
104 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
114 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
105 :class => 'changeset', :title => 'My very first commit')
115 :class => 'changeset', :title => 'My very first commit')
106
116
107 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
117 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
108 :class => 'document')
118 :class => 'document')
109
119
110 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
120 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
111 :class => 'version')
121 :class => 'version')
112
122
113 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
123 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
114
124
115 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
125 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
116 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
126 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
117
127
118 to_test = {
128 to_test = {
119 # tickets
129 # tickets
120 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
130 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
121 # changesets
131 # changesets
122 'r1' => changeset_link,
132 'r1' => changeset_link,
123 # documents
133 # documents
124 'document#1' => document_link,
134 'document#1' => document_link,
125 'document:"Test document"' => document_link,
135 'document:"Test document"' => document_link,
126 # versions
136 # versions
127 'version#2' => version_link,
137 'version#2' => version_link,
128 'version:1.0' => version_link,
138 'version:1.0' => version_link,
129 'version:"1.0"' => version_link,
139 'version:"1.0"' => version_link,
130 # source
140 # source
131 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
141 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
132 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
142 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
133 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
143 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
134 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
144 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
135 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
145 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
136 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
146 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
137 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
147 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
138 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
148 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
139 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
149 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
140 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
150 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
141 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
151 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
142 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
152 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
143 # message
153 # message
144 'message#4' => link_to('Post 2', message_url, :class => 'message'),
154 'message#4' => link_to('Post 2', message_url, :class => 'message'),
145 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
155 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
146 # escaping
156 # escaping
147 '!#3.' => '#3.',
157 '!#3.' => '#3.',
148 '!r1' => 'r1',
158 '!r1' => 'r1',
149 '!document#1' => 'document#1',
159 '!document#1' => 'document#1',
150 '!document:"Test document"' => 'document:"Test document"',
160 '!document:"Test document"' => 'document:"Test document"',
151 '!version#2' => 'version#2',
161 '!version#2' => 'version#2',
152 '!version:1.0' => 'version:1.0',
162 '!version:1.0' => 'version:1.0',
153 '!version:"1.0"' => 'version:"1.0"',
163 '!version:"1.0"' => 'version:"1.0"',
154 '!source:/some/file' => 'source:/some/file',
164 '!source:/some/file' => 'source:/some/file',
155 # invalid expressions
165 # invalid expressions
156 'source:' => 'source:',
166 'source:' => 'source:',
157 # url hash
167 # url hash
158 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
168 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
159 }
169 }
160 @project = Project.find(1)
170 @project = Project.find(1)
161 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
171 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
162 end
172 end
163
173
164 def test_wiki_links
174 def test_wiki_links
165 to_test = {
175 to_test = {
166 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
176 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
167 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
177 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
168 # link with anchor
178 # link with anchor
169 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
179 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
170 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
180 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
171 # page that doesn't exist
181 # page that doesn't exist
172 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
182 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
173 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
183 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
174 # link to another project wiki
184 # link to another project wiki
175 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
185 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
176 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
186 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
177 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
187 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
178 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
188 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
179 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
189 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
180 # striked through link
190 # striked through link
181 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
191 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
182 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
192 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
183 # escaping
193 # escaping
184 '![[Another page|Page]]' => '[[Another page|Page]]',
194 '![[Another page|Page]]' => '[[Another page|Page]]',
185 }
195 }
186 @project = Project.find(1)
196 @project = Project.find(1)
187 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
197 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
188 end
198 end
189
199
190 def test_html_tags
200 def test_html_tags
191 to_test = {
201 to_test = {
192 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
202 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
193 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
203 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
194 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
204 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
195 # do not escape pre/code tags
205 # do not escape pre/code tags
196 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
206 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
197 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
207 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
198 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
208 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
199 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
209 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
200 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
210 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
201 # remove attributes except class
211 # remove attributes except class
202 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
212 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
203 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
213 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
204 }
214 }
205 to_test.each { |text, result| assert_equal result, textilizable(text) }
215 to_test.each { |text, result| assert_equal result, textilizable(text) }
206 end
216 end
207
217
208 def test_allowed_html_tags
218 def test_allowed_html_tags
209 to_test = {
219 to_test = {
210 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
220 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
211 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
221 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
212 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
222 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
213 }
223 }
214 to_test.each { |text, result| assert_equal result, textilizable(text) }
224 to_test.each { |text, result| assert_equal result, textilizable(text) }
215 end
225 end
216
226
217 def syntax_highlight
227 def syntax_highlight
218 raw = <<-RAW
228 raw = <<-RAW
219 <pre><code class="ruby">
229 <pre><code class="ruby">
220 # Some ruby code here
230 # Some ruby code here
221 </pre></code>
231 </pre></code>
222 RAW
232 RAW
223
233
224 expected = <<-EXPECTED
234 expected = <<-EXPECTED
225 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
235 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
226 </pre></code>
236 </pre></code>
227 EXPECTED
237 EXPECTED
228
238
229 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
239 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
230 end
240 end
231
241
232 def test_wiki_links_in_tables
242 def test_wiki_links_in_tables
233 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
243 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
234 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
244 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
235 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
245 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
236 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
246 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
237 }
247 }
238 @project = Project.find(1)
248 @project = Project.find(1)
239 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
249 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
240 end
250 end
241
251
242 def test_text_formatting
252 def test_text_formatting
243 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
253 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
244 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
254 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
245 }
255 }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
256 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
247 end
257 end
248
258
249 def test_wiki_horizontal_rule
259 def test_wiki_horizontal_rule
250 assert_equal '<hr />', textilizable('---')
260 assert_equal '<hr />', textilizable('---')
251 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
261 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
252 end
262 end
253
263
254 def test_acronym
264 def test_acronym
255 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
265 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
256 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
266 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
257 end
267 end
258
268
259 def test_footnotes
269 def test_footnotes
260 raw = <<-RAW
270 raw = <<-RAW
261 This is some text[1].
271 This is some text[1].
262
272
263 fn1. This is the foot note
273 fn1. This is the foot note
264 RAW
274 RAW
265
275
266 expected = <<-EXPECTED
276 expected = <<-EXPECTED
267 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
277 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
268 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
278 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
269 EXPECTED
279 EXPECTED
270
280
271 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
281 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
272 end
282 end
273
283
274 def test_table_of_content
284 def test_table_of_content
275 raw = <<-RAW
285 raw = <<-RAW
276 {{toc}}
286 {{toc}}
277
287
278 h1. Title
288 h1. Title
279
289
280 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
290 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
281
291
282 h2. Subtitle
292 h2. Subtitle
283
293
284 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
294 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
285
295
286 h2. Subtitle with %{color:red}red text%
296 h2. Subtitle with %{color:red}red text%
287
297
288 h1. Another title
298 h1. Another title
289
299
290 RAW
300 RAW
291
301
292 expected = '<ul class="toc">' +
302 expected = '<ul class="toc">' +
293 '<li class="heading1"><a href="#Title">Title</a></li>' +
303 '<li class="heading1"><a href="#Title">Title</a></li>' +
294 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
304 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
295 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
305 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
296 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
306 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
297 '</ul>'
307 '</ul>'
298
308
299 assert textilizable(raw).gsub("\n", "").include?(expected)
309 assert textilizable(raw).gsub("\n", "").include?(expected)
300 end
310 end
301
311
302 def test_blockquote
312 def test_blockquote
303 # orig raw text
313 # orig raw text
304 raw = <<-RAW
314 raw = <<-RAW
305 John said:
315 John said:
306 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
316 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
307 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
317 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
308 > * Donec odio lorem,
318 > * Donec odio lorem,
309 > * sagittis ac,
319 > * sagittis ac,
310 > * malesuada in,
320 > * malesuada in,
311 > * adipiscing eu, dolor.
321 > * adipiscing eu, dolor.
312 >
322 >
313 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
323 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
314 > Proin a tellus. Nam vel neque.
324 > Proin a tellus. Nam vel neque.
315
325
316 He's right.
326 He's right.
317 RAW
327 RAW
318
328
319 # expected html
329 # expected html
320 expected = <<-EXPECTED
330 expected = <<-EXPECTED
321 <p>John said:</p>
331 <p>John said:</p>
322 <blockquote>
332 <blockquote>
323 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
333 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
324 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
334 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
325 <ul>
335 <ul>
326 <li>Donec odio lorem,</li>
336 <li>Donec odio lorem,</li>
327 <li>sagittis ac,</li>
337 <li>sagittis ac,</li>
328 <li>malesuada in,</li>
338 <li>malesuada in,</li>
329 <li>adipiscing eu, dolor.</li>
339 <li>adipiscing eu, dolor.</li>
330 </ul>
340 </ul>
331 <blockquote>
341 <blockquote>
332 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
342 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
333 </blockquote>
343 </blockquote>
334 <p>Proin a tellus. Nam vel neque.</p>
344 <p>Proin a tellus. Nam vel neque.</p>
335 </blockquote>
345 </blockquote>
336 <p>He's right.</p>
346 <p>He's right.</p>
337 EXPECTED
347 EXPECTED
338
348
339 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
349 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
340 end
350 end
341
351
342 def test_table
352 def test_table
343 raw = <<-RAW
353 raw = <<-RAW
344 This is a table with empty cells:
354 This is a table with empty cells:
345
355
346 |cell11|cell12||
356 |cell11|cell12||
347 |cell21||cell23|
357 |cell21||cell23|
348 |cell31|cell32|cell33|
358 |cell31|cell32|cell33|
349 RAW
359 RAW
350
360
351 expected = <<-EXPECTED
361 expected = <<-EXPECTED
352 <p>This is a table with empty cells:</p>
362 <p>This is a table with empty cells:</p>
353
363
354 <table>
364 <table>
355 <tr><td>cell11</td><td>cell12</td><td></td></tr>
365 <tr><td>cell11</td><td>cell12</td><td></td></tr>
356 <tr><td>cell21</td><td></td><td>cell23</td></tr>
366 <tr><td>cell21</td><td></td><td>cell23</td></tr>
357 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
367 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
358 </table>
368 </table>
359 EXPECTED
369 EXPECTED
360
370
361 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
371 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
362 end
372 end
363
373
364 def test_default_formatter
374 def test_default_formatter
365 Setting.text_formatting = 'unknown'
375 Setting.text_formatting = 'unknown'
366 text = 'a *link*: http://www.example.net/'
376 text = 'a *link*: http://www.example.net/'
367 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
377 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
368 Setting.text_formatting = 'textile'
378 Setting.text_formatting = 'textile'
369 end
379 end
370
380
371 def test_date_format_default
381 def test_date_format_default
372 today = Date.today
382 today = Date.today
373 Setting.date_format = ''
383 Setting.date_format = ''
374 assert_equal l_date(today), format_date(today)
384 assert_equal l_date(today), format_date(today)
375 end
385 end
376
386
377 def test_date_format
387 def test_date_format
378 today = Date.today
388 today = Date.today
379 Setting.date_format = '%d %m %Y'
389 Setting.date_format = '%d %m %Y'
380 assert_equal today.strftime('%d %m %Y'), format_date(today)
390 assert_equal today.strftime('%d %m %Y'), format_date(today)
381 end
391 end
382
392
383 def test_time_format_default
393 def test_time_format_default
384 now = Time.now
394 now = Time.now
385 Setting.date_format = ''
395 Setting.date_format = ''
386 Setting.time_format = ''
396 Setting.time_format = ''
387 assert_equal l_datetime(now), format_time(now)
397 assert_equal l_datetime(now), format_time(now)
388 assert_equal l_time(now), format_time(now, false)
398 assert_equal l_time(now), format_time(now, false)
389 end
399 end
390
400
391 def test_time_format
401 def test_time_format
392 now = Time.now
402 now = Time.now
393 Setting.date_format = '%d %m %Y'
403 Setting.date_format = '%d %m %Y'
394 Setting.time_format = '%H %M'
404 Setting.time_format = '%H %M'
395 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
405 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
396 assert_equal now.strftime('%H %M'), format_time(now, false)
406 assert_equal now.strftime('%H %M'), format_time(now, false)
397 end
407 end
398
408
399 def test_utc_time_format
409 def test_utc_time_format
400 now = Time.now.utc
410 now = Time.now.utc
401 Setting.date_format = '%d %m %Y'
411 Setting.date_format = '%d %m %Y'
402 Setting.time_format = '%H %M'
412 Setting.time_format = '%H %M'
403 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
413 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
404 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
414 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
405 end
415 end
406
416
407 def test_due_date_distance_in_words
417 def test_due_date_distance_in_words
408 to_test = { Date.today => 'Due in 0 days',
418 to_test = { Date.today => 'Due in 0 days',
409 Date.today + 1 => 'Due in 1 day',
419 Date.today + 1 => 'Due in 1 day',
410 Date.today + 100 => 'Due in 100 days',
420 Date.today + 100 => 'Due in 100 days',
411 Date.today + 20000 => 'Due in 20000 days',
421 Date.today + 20000 => 'Due in 20000 days',
412 Date.today - 1 => '1 day late',
422 Date.today - 1 => '1 day late',
413 Date.today - 100 => '100 days late',
423 Date.today - 100 => '100 days late',
414 Date.today - 20000 => '20000 days late',
424 Date.today - 20000 => '20000 days late',
415 }
425 }
416 to_test.each do |date, expected|
426 to_test.each do |date, expected|
417 assert_equal expected, due_date_distance_in_words(date)
427 assert_equal expected, due_date_distance_in_words(date)
418 end
428 end
419 end
429 end
420
430
421 def test_avatar
431 def test_avatar
422 # turn on avatars
432 # turn on avatars
423 Setting.gravatar_enabled = '1'
433 Setting.gravatar_enabled = '1'
424 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
434 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
425 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
435 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
426 assert_nil avatar('jsmith')
436 assert_nil avatar('jsmith')
427 assert_nil avatar(nil)
437 assert_nil avatar(nil)
428
438
429 # turn off avatars
439 # turn off avatars
430 Setting.gravatar_enabled = '0'
440 Setting.gravatar_enabled = '0'
431 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
441 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
432 end
442 end
433 end
443 end
General Comments 0
You need to be logged in to leave comments. Login now