##// END OF EJS Templates
Lists can be reordered with drag and drop (#12909)....
Jean-Philippe Lang -
r14954:42b5c332b2c2
parent child
Show More
@@ -1,88 +1,96
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 CustomFieldsController < ApplicationController
18 class CustomFieldsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin
21 before_filter :require_admin
22 before_filter :build_new_custom_field, :only => [:new, :create]
22 before_filter :build_new_custom_field, :only => [:new, :create]
23 before_filter :find_custom_field, :only => [:edit, :update, :destroy]
23 before_filter :find_custom_field, :only => [:edit, :update, :destroy]
24 accept_api_auth :index
24 accept_api_auth :index
25
25
26 def index
26 def index
27 respond_to do |format|
27 respond_to do |format|
28 format.html {
28 format.html {
29 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
29 @custom_fields_by_type = CustomField.all.group_by {|f| f.class.name }
30 }
30 }
31 format.api {
31 format.api {
32 @custom_fields = CustomField.all
32 @custom_fields = CustomField.all
33 }
33 }
34 end
34 end
35 end
35 end
36
36
37 def new
37 def new
38 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
38 @custom_field.field_format = 'string' if @custom_field.field_format.blank?
39 @custom_field.default_value = nil
39 @custom_field.default_value = nil
40 end
40 end
41
41
42 def create
42 def create
43 if @custom_field.save
43 if @custom_field.save
44 flash[:notice] = l(:notice_successful_create)
44 flash[:notice] = l(:notice_successful_create)
45 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
45 call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
46 redirect_to edit_custom_field_path(@custom_field)
46 redirect_to edit_custom_field_path(@custom_field)
47 else
47 else
48 render :action => 'new'
48 render :action => 'new'
49 end
49 end
50 end
50 end
51
51
52 def edit
52 def edit
53 end
53 end
54
54
55 def update
55 def update
56 if @custom_field.update_attributes(params[:custom_field])
56 if @custom_field.update_attributes(params[:custom_field])
57 flash[:notice] = l(:notice_successful_update)
58 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
57 call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
59 redirect_back_or_default edit_custom_field_path(@custom_field)
58 respond_to do |format|
59 format.html {
60 flash[:notice] = l(:notice_successful_update)
61 redirect_back_or_default edit_custom_field_path(@custom_field)
62 }
63 format.js { render :nothing => true }
64 end
60 else
65 else
61 render :action => 'edit'
66 respond_to do |format|
67 format.html { render :action => 'edit' }
68 format.js { render :nothing => true, :status => 422 }
69 end
62 end
70 end
63 end
71 end
64
72
65 def destroy
73 def destroy
66 begin
74 begin
67 @custom_field.destroy
75 @custom_field.destroy
68 rescue
76 rescue
69 flash[:error] = l(:error_can_not_delete_custom_field)
77 flash[:error] = l(:error_can_not_delete_custom_field)
70 end
78 end
71 redirect_to custom_fields_path(:tab => @custom_field.class.name)
79 redirect_to custom_fields_path(:tab => @custom_field.class.name)
72 end
80 end
73
81
74 private
82 private
75
83
76 def build_new_custom_field
84 def build_new_custom_field
77 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
85 @custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
78 if @custom_field.nil?
86 if @custom_field.nil?
79 render :action => 'select_type'
87 render :action => 'select_type'
80 end
88 end
81 end
89 end
82
90
83 def find_custom_field
91 def find_custom_field
84 @custom_field = CustomField.find(params[:id])
92 @custom_field = CustomField.find(params[:id])
85 rescue ActiveRecord::RecordNotFound
93 rescue ActiveRecord::RecordNotFound
86 render_404
94 render_404
87 end
95 end
88 end
96 end
@@ -1,96 +1,104
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 EnumerationsController < ApplicationController
18 class EnumerationsController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :index
21 before_filter :require_admin, :except => :index
22 before_filter :require_admin_or_api_request, :only => :index
22 before_filter :require_admin_or_api_request, :only => :index
23 before_filter :build_new_enumeration, :only => [:new, :create]
23 before_filter :build_new_enumeration, :only => [:new, :create]
24 before_filter :find_enumeration, :only => [:edit, :update, :destroy]
24 before_filter :find_enumeration, :only => [:edit, :update, :destroy]
25 accept_api_auth :index
25 accept_api_auth :index
26
26
27 helper :custom_fields
27 helper :custom_fields
28
28
29 def index
29 def index
30 respond_to do |format|
30 respond_to do |format|
31 format.html
31 format.html
32 format.api {
32 format.api {
33 @klass = Enumeration.get_subclass(params[:type])
33 @klass = Enumeration.get_subclass(params[:type])
34 if @klass
34 if @klass
35 @enumerations = @klass.shared.sorted.to_a
35 @enumerations = @klass.shared.sorted.to_a
36 else
36 else
37 render_404
37 render_404
38 end
38 end
39 }
39 }
40 end
40 end
41 end
41 end
42
42
43 def new
43 def new
44 end
44 end
45
45
46 def create
46 def create
47 if request.post? && @enumeration.save
47 if request.post? && @enumeration.save
48 flash[:notice] = l(:notice_successful_create)
48 flash[:notice] = l(:notice_successful_create)
49 redirect_to enumerations_path
49 redirect_to enumerations_path
50 else
50 else
51 render :action => 'new'
51 render :action => 'new'
52 end
52 end
53 end
53 end
54
54
55 def edit
55 def edit
56 end
56 end
57
57
58 def update
58 def update
59 if @enumeration.update_attributes(params[:enumeration])
59 if @enumeration.update_attributes(params[:enumeration])
60 flash[:notice] = l(:notice_successful_update)
60 respond_to do |format|
61 redirect_to enumerations_path
61 format.html {
62 flash[:notice] = l(:notice_successful_update)
63 redirect_to enumerations_path
64 }
65 format.js { render :nothing => true }
66 end
62 else
67 else
63 render :action => 'edit'
68 respond_to do |format|
69 format.html { render :action => 'edit' }
70 format.js { render :nothing => true, :status => 422 }
71 end
64 end
72 end
65 end
73 end
66
74
67 def destroy
75 def destroy
68 if !@enumeration.in_use?
76 if !@enumeration.in_use?
69 # No associated objects
77 # No associated objects
70 @enumeration.destroy
78 @enumeration.destroy
71 redirect_to enumerations_path
79 redirect_to enumerations_path
72 return
80 return
73 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
81 elsif params[:reassign_to_id].present? && (reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id].to_i))
74 @enumeration.destroy(reassign_to)
82 @enumeration.destroy(reassign_to)
75 redirect_to enumerations_path
83 redirect_to enumerations_path
76 return
84 return
77 end
85 end
78 @enumerations = @enumeration.class.system.to_a - [@enumeration]
86 @enumerations = @enumeration.class.system.to_a - [@enumeration]
79 end
87 end
80
88
81 private
89 private
82
90
83 def build_new_enumeration
91 def build_new_enumeration
84 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
92 class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
85 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
93 @enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
86 if @enumeration.nil?
94 if @enumeration.nil?
87 render_404
95 render_404
88 end
96 end
89 end
97 end
90
98
91 def find_enumeration
99 def find_enumeration
92 @enumeration = Enumeration.find(params[:id])
100 @enumeration = Enumeration.find(params[:id])
93 rescue ActiveRecord::RecordNotFound
101 rescue ActiveRecord::RecordNotFound
94 render_404
102 render_404
95 end
103 end
96 end
104 end
@@ -1,77 +1,85
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 IssueStatusesController < ApplicationController
18 class IssueStatusesController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :index
21 before_filter :require_admin, :except => :index
22 before_filter :require_admin_or_api_request, :only => :index
22 before_filter :require_admin_or_api_request, :only => :index
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 def index
25 def index
26 @issue_statuses = IssueStatus.sorted.to_a
26 @issue_statuses = IssueStatus.sorted.to_a
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :layout => false if request.xhr? }
28 format.html { render :layout => false if request.xhr? }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def new
33 def new
34 @issue_status = IssueStatus.new
34 @issue_status = IssueStatus.new
35 end
35 end
36
36
37 def create
37 def create
38 @issue_status = IssueStatus.new(params[:issue_status])
38 @issue_status = IssueStatus.new(params[:issue_status])
39 if @issue_status.save
39 if @issue_status.save
40 flash[:notice] = l(:notice_successful_create)
40 flash[:notice] = l(:notice_successful_create)
41 redirect_to issue_statuses_path
41 redirect_to issue_statuses_path
42 else
42 else
43 render :action => 'new'
43 render :action => 'new'
44 end
44 end
45 end
45 end
46
46
47 def edit
47 def edit
48 @issue_status = IssueStatus.find(params[:id])
48 @issue_status = IssueStatus.find(params[:id])
49 end
49 end
50
50
51 def update
51 def update
52 @issue_status = IssueStatus.find(params[:id])
52 @issue_status = IssueStatus.find(params[:id])
53 if @issue_status.update_attributes(params[:issue_status])
53 if @issue_status.update_attributes(params[:issue_status])
54 flash[:notice] = l(:notice_successful_update)
54 respond_to do |format|
55 redirect_to issue_statuses_path(:page => params[:page])
55 format.html {
56 flash[:notice] = l(:notice_successful_update)
57 redirect_to issue_statuses_path(:page => params[:page])
58 }
59 format.js { render :nothing => true }
60 end
56 else
61 else
57 render :action => 'edit'
62 respond_to do |format|
63 format.html { render :action => 'edit' }
64 format.js { render :nothing => true, :status => 422 }
65 end
58 end
66 end
59 end
67 end
60
68
61 def destroy
69 def destroy
62 IssueStatus.find(params[:id]).destroy
70 IssueStatus.find(params[:id]).destroy
63 redirect_to issue_statuses_path
71 redirect_to issue_statuses_path
64 rescue
72 rescue
65 flash[:error] = l(:error_unable_delete_issue_status)
73 flash[:error] = l(:error_unable_delete_issue_status)
66 redirect_to issue_statuses_path
74 redirect_to issue_statuses_path
67 end
75 end
68
76
69 def update_issue_done_ratio
77 def update_issue_done_ratio
70 if request.post? && IssueStatus.update_issue_done_ratios
78 if request.post? && IssueStatus.update_issue_done_ratios
71 flash[:notice] = l(:notice_issue_done_ratios_updated)
79 flash[:notice] = l(:notice_issue_done_ratios_updated)
72 else
80 else
73 flash[:error] = l(:error_issue_done_ratios_not_updated)
81 flash[:error] = l(:error_issue_done_ratios_not_updated)
74 end
82 end
75 redirect_to issue_statuses_path
83 redirect_to issue_statuses_path
76 end
84 end
77 end
85 end
@@ -1,110 +1,118
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 RolesController < ApplicationController
18 class RolesController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => [:index, :show]
21 before_filter :require_admin, :except => [:index, :show]
22 before_filter :require_admin_or_api_request, :only => [:index, :show]
22 before_filter :require_admin_or_api_request, :only => [:index, :show]
23 before_filter :find_role, :only => [:show, :edit, :update, :destroy]
23 before_filter :find_role, :only => [:show, :edit, :update, :destroy]
24 accept_api_auth :index, :show
24 accept_api_auth :index, :show
25
25
26 require_sudo_mode :create, :update, :destroy
26 require_sudo_mode :create, :update, :destroy
27
27
28 def index
28 def index
29 respond_to do |format|
29 respond_to do |format|
30 format.html {
30 format.html {
31 @roles = Role.sorted.to_a
31 @roles = Role.sorted.to_a
32 render :layout => false if request.xhr?
32 render :layout => false if request.xhr?
33 }
33 }
34 format.api {
34 format.api {
35 @roles = Role.givable.to_a
35 @roles = Role.givable.to_a
36 }
36 }
37 end
37 end
38 end
38 end
39
39
40 def show
40 def show
41 respond_to do |format|
41 respond_to do |format|
42 format.api
42 format.api
43 end
43 end
44 end
44 end
45
45
46 def new
46 def new
47 # Prefills the form with 'Non member' role permissions by default
47 # Prefills the form with 'Non member' role permissions by default
48 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
48 @role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
49 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
49 if params[:copy].present? && @copy_from = Role.find_by_id(params[:copy])
50 @role.copy_from(@copy_from)
50 @role.copy_from(@copy_from)
51 end
51 end
52 @roles = Role.sorted.to_a
52 @roles = Role.sorted.to_a
53 end
53 end
54
54
55 def create
55 def create
56 @role = Role.new(params[:role])
56 @role = Role.new(params[:role])
57 if request.post? && @role.save
57 if request.post? && @role.save
58 # workflow copy
58 # workflow copy
59 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
59 if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
60 @role.workflow_rules.copy(copy_from)
60 @role.workflow_rules.copy(copy_from)
61 end
61 end
62 flash[:notice] = l(:notice_successful_create)
62 flash[:notice] = l(:notice_successful_create)
63 redirect_to roles_path
63 redirect_to roles_path
64 else
64 else
65 @roles = Role.sorted.to_a
65 @roles = Role.sorted.to_a
66 render :action => 'new'
66 render :action => 'new'
67 end
67 end
68 end
68 end
69
69
70 def edit
70 def edit
71 end
71 end
72
72
73 def update
73 def update
74 if @role.update_attributes(params[:role])
74 if @role.update_attributes(params[:role])
75 flash[:notice] = l(:notice_successful_update)
75 respond_to do |format|
76 redirect_to roles_path(:page => params[:page])
76 format.html {
77 flash[:notice] = l(:notice_successful_update)
78 redirect_to roles_path(:page => params[:page])
79 }
80 format.js { render :nothing => true }
81 end
77 else
82 else
78 render :action => 'edit'
83 respond_to do |format|
84 format.html { render :action => 'edit' }
85 format.js { render :nothing => true, :status => 422 }
86 end
79 end
87 end
80 end
88 end
81
89
82 def destroy
90 def destroy
83 @role.destroy
91 @role.destroy
84 redirect_to roles_path
92 redirect_to roles_path
85 rescue
93 rescue
86 flash[:error] = l(:error_can_not_remove_role)
94 flash[:error] = l(:error_can_not_remove_role)
87 redirect_to roles_path
95 redirect_to roles_path
88 end
96 end
89
97
90 def permissions
98 def permissions
91 @roles = Role.sorted.to_a
99 @roles = Role.sorted.to_a
92 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
100 @permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
93 if request.post?
101 if request.post?
94 @roles.each do |role|
102 @roles.each do |role|
95 role.permissions = params[:permissions][role.id.to_s]
103 role.permissions = params[:permissions][role.id.to_s]
96 role.save
104 role.save
97 end
105 end
98 flash[:notice] = l(:notice_successful_update)
106 flash[:notice] = l(:notice_successful_update)
99 redirect_to roles_path
107 redirect_to roles_path
100 end
108 end
101 end
109 end
102
110
103 private
111 private
104
112
105 def find_role
113 def find_role
106 @role = Role.find(params[:id])
114 @role = Role.find(params[:id])
107 rescue ActiveRecord::RecordNotFound
115 rescue ActiveRecord::RecordNotFound
108 render_404
116 render_404
109 end
117 end
110 end
118 end
@@ -1,97 +1,107
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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 TrackersController < ApplicationController
18 class TrackersController < ApplicationController
19 layout 'admin'
19 layout 'admin'
20
20
21 before_filter :require_admin, :except => :index
21 before_filter :require_admin, :except => :index
22 before_filter :require_admin_or_api_request, :only => :index
22 before_filter :require_admin_or_api_request, :only => :index
23 accept_api_auth :index
23 accept_api_auth :index
24
24
25 def index
25 def index
26 @trackers = Tracker.sorted.to_a
26 @trackers = Tracker.sorted.to_a
27 respond_to do |format|
27 respond_to do |format|
28 format.html { render :layout => false if request.xhr? }
28 format.html { render :layout => false if request.xhr? }
29 format.api
29 format.api
30 end
30 end
31 end
31 end
32
32
33 def new
33 def new
34 @tracker ||= Tracker.new(params[:tracker])
34 @tracker ||= Tracker.new(params[:tracker])
35 @trackers = Tracker.sorted.to_a
35 @trackers = Tracker.sorted.to_a
36 @projects = Project.all
36 @projects = Project.all
37 end
37 end
38
38
39 def create
39 def create
40 @tracker = Tracker.new(params[:tracker])
40 @tracker = Tracker.new(params[:tracker])
41 if @tracker.save
41 if @tracker.save
42 # workflow copy
42 # workflow copy
43 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
43 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
44 @tracker.workflow_rules.copy(copy_from)
44 @tracker.workflow_rules.copy(copy_from)
45 end
45 end
46 flash[:notice] = l(:notice_successful_create)
46 flash[:notice] = l(:notice_successful_create)
47 redirect_to trackers_path
47 redirect_to trackers_path
48 return
48 return
49 end
49 end
50 new
50 new
51 render :action => 'new'
51 render :action => 'new'
52 end
52 end
53
53
54 def edit
54 def edit
55 @tracker ||= Tracker.find(params[:id])
55 @tracker ||= Tracker.find(params[:id])
56 @projects = Project.all
56 @projects = Project.all
57 end
57 end
58
58
59 def update
59 def update
60 @tracker = Tracker.find(params[:id])
60 @tracker = Tracker.find(params[:id])
61 if @tracker.update_attributes(params[:tracker])
61 if @tracker.update_attributes(params[:tracker])
62 flash[:notice] = l(:notice_successful_update)
62 respond_to do |format|
63 redirect_to trackers_path(:page => params[:page])
63 format.html {
64 return
64 flash[:notice] = l(:notice_successful_update)
65 redirect_to trackers_path(:page => params[:page])
66 }
67 format.js { render :nothing => true }
68 end
69 else
70 respond_to do |format|
71 format.html {
72 edit
73 render :action => 'edit'
74 }
75 format.js { render :nothing => true, :status => 422 }
76 end
65 end
77 end
66 edit
67 render :action => 'edit'
68 end
78 end
69
79
70 def destroy
80 def destroy
71 @tracker = Tracker.find(params[:id])
81 @tracker = Tracker.find(params[:id])
72 unless @tracker.issues.empty?
82 unless @tracker.issues.empty?
73 flash[:error] = l(:error_can_not_delete_tracker)
83 flash[:error] = l(:error_can_not_delete_tracker)
74 else
84 else
75 @tracker.destroy
85 @tracker.destroy
76 end
86 end
77 redirect_to trackers_path
87 redirect_to trackers_path
78 end
88 end
79
89
80 def fields
90 def fields
81 if request.post? && params[:trackers]
91 if request.post? && params[:trackers]
82 params[:trackers].each do |tracker_id, tracker_params|
92 params[:trackers].each do |tracker_id, tracker_params|
83 tracker = Tracker.find_by_id(tracker_id)
93 tracker = Tracker.find_by_id(tracker_id)
84 if tracker
94 if tracker
85 tracker.core_fields = tracker_params[:core_fields]
95 tracker.core_fields = tracker_params[:core_fields]
86 tracker.custom_field_ids = tracker_params[:custom_field_ids]
96 tracker.custom_field_ids = tracker_params[:custom_field_ids]
87 tracker.save
97 tracker.save
88 end
98 end
89 end
99 end
90 flash[:notice] = l(:notice_successful_update)
100 flash[:notice] = l(:notice_successful_update)
91 redirect_to fields_trackers_path
101 redirect_to fields_trackers_path
92 return
102 return
93 end
103 end
94 @trackers = Tracker.sorted.to_a
104 @trackers = Tracker.sorted.to_a
95 @custom_fields = IssueCustomField.all.sort
105 @custom_fields = IssueCustomField.all.sort
96 end
106 end
97 end
107 end
@@ -1,1346 +1,1356
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31
31
32 extend Forwardable
32 extend Forwardable
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34
34
35 # Return true if user is authorized for controller/action, otherwise false
35 # Return true if user is authorized for controller/action, otherwise false
36 def authorize_for(controller, action)
36 def authorize_for(controller, action)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 end
38 end
39
39
40 # Display a link if user is authorized
40 # Display a link if user is authorized
41 #
41 #
42 # @param [String] name Anchor text (passed to link_to)
42 # @param [String] name Anchor text (passed to link_to)
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [optional, Hash] html_options Options passed to link_to
44 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active? || (User.current.admin? && user.logged?)
54 if user.active? || (User.current.admin? && user.logged?)
55 link_to name, user_path(user), :class => user.css_classes
55 link_to name, user_path(user), :class => user.css_classes
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 #
72 #
73 def link_to_issue(issue, options={})
73 def link_to_issue(issue, options={})
74 title = nil
74 title = nil
75 subject = nil
75 subject = nil
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 if options[:subject] == false
77 if options[:subject] == false
78 title = issue.subject.truncate(60)
78 title = issue.subject.truncate(60)
79 else
79 else
80 subject = issue.subject
80 subject = issue.subject
81 if truncate_length = options[:truncate]
81 if truncate_length = options[:truncate]
82 subject = subject.truncate(truncate_length)
82 subject = subject.truncate(truncate_length)
83 end
83 end
84 end
84 end
85 only_path = options[:only_path].nil? ? true : options[:only_path]
85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 s = link_to(text, issue_url(issue, :only_path => only_path),
86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 :class => issue.css_classes, :title => title)
87 :class => issue.css_classes, :title => title)
88 s << h(": #{subject}") if subject
88 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
89 s = h("#{issue.project} - ") + s if options[:project]
90 s
90 s
91 end
91 end
92
92
93 # Generates a link to an attachment.
93 # Generates a link to an attachment.
94 # Options:
94 # Options:
95 # * :text - Link text (default to attachment filename)
95 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
96 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
97 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
98 text = options.delete(:text) || attachment.filename
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 html_options = options.slice!(:only_path)
100 html_options = options.slice!(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
102 url = send(route_method, attachment, attachment.filename, options)
102 url = send(route_method, attachment, attachment.filename, options)
103 link_to text, url, html_options
103 link_to text, url, html_options
104 end
104 end
105
105
106 # Generates a link to a SCM revision
106 # Generates a link to a SCM revision
107 # Options:
107 # Options:
108 # * :text - Link text (default to the formatted revision)
108 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, repository, options={})
109 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
110 if repository.is_a?(Project)
111 repository = repository.repository
111 repository = repository.repository
112 end
112 end
113 text = options.delete(:text) || format_revision(revision)
113 text = options.delete(:text) || format_revision(revision)
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 link_to(
115 link_to(
116 h(text),
116 h(text),
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 :title => l(:label_revision_id, format_revision(revision)),
118 :title => l(:label_revision_id, format_revision(revision)),
119 :accesskey => options[:accesskey]
119 :accesskey => options[:accesskey]
120 )
120 )
121 end
121 end
122
122
123 # Generates a link to a message
123 # Generates a link to a message
124 def link_to_message(message, options={}, html_options = nil)
124 def link_to_message(message, options={}, html_options = nil)
125 link_to(
125 link_to(
126 message.subject.truncate(60),
126 message.subject.truncate(60),
127 board_message_url(message.board_id, message.parent_id || message.id, {
127 board_message_url(message.board_id, message.parent_id || message.id, {
128 :r => (message.parent_id && message.id),
128 :r => (message.parent_id && message.id),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :only_path => true
130 :only_path => true
131 }.merge(options)),
131 }.merge(options)),
132 html_options
132 html_options
133 )
133 )
134 end
134 end
135
135
136 # Generates a link to a project if active
136 # Generates a link to a project if active
137 # Examples:
137 # Examples:
138 #
138 #
139 # link_to_project(project) # => link to the specified project overview
139 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 #
142 #
143 def link_to_project(project, options={}, html_options = nil)
143 def link_to_project(project, options={}, html_options = nil)
144 if project.archived?
144 if project.archived?
145 h(project.name)
145 h(project.name)
146 else
146 else
147 link_to project.name,
147 link_to project.name,
148 project_url(project, {:only_path => true}.merge(options)),
148 project_url(project, {:only_path => true}.merge(options)),
149 html_options
149 html_options
150 end
150 end
151 end
151 end
152
152
153 # Generates a link to a project settings if active
153 # Generates a link to a project settings if active
154 def link_to_project_settings(project, options={}, html_options=nil)
154 def link_to_project_settings(project, options={}, html_options=nil)
155 if project.active?
155 if project.active?
156 link_to project.name, settings_project_path(project, options), html_options
156 link_to project.name, settings_project_path(project, options), html_options
157 elsif project.archived?
157 elsif project.archived?
158 h(project.name)
158 h(project.name)
159 else
159 else
160 link_to project.name, project_path(project, options), html_options
160 link_to project.name, project_path(project, options), html_options
161 end
161 end
162 end
162 end
163
163
164 # Generates a link to a version
164 # Generates a link to a version
165 def link_to_version(version, options = {})
165 def link_to_version(version, options = {})
166 return '' unless version && version.is_a?(Version)
166 return '' unless version && version.is_a?(Version)
167 options = {:title => format_date(version.effective_date)}.merge(options)
167 options = {:title => format_date(version.effective_date)}.merge(options)
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 end
169 end
170
170
171 # Helper that formats object for html or text rendering
171 # Helper that formats object for html or text rendering
172 def format_object(object, html=true, &block)
172 def format_object(object, html=true, &block)
173 if block_given?
173 if block_given?
174 object = yield object
174 object = yield object
175 end
175 end
176 case object.class.name
176 case object.class.name
177 when 'Array'
177 when 'Array'
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 when 'Time'
179 when 'Time'
180 format_time(object)
180 format_time(object)
181 when 'Date'
181 when 'Date'
182 format_date(object)
182 format_date(object)
183 when 'Fixnum'
183 when 'Fixnum'
184 object.to_s
184 object.to_s
185 when 'Float'
185 when 'Float'
186 sprintf "%.2f", object
186 sprintf "%.2f", object
187 when 'User'
187 when 'User'
188 html ? link_to_user(object) : object.to_s
188 html ? link_to_user(object) : object.to_s
189 when 'Project'
189 when 'Project'
190 html ? link_to_project(object) : object.to_s
190 html ? link_to_project(object) : object.to_s
191 when 'Version'
191 when 'Version'
192 html ? link_to_version(object) : object.to_s
192 html ? link_to_version(object) : object.to_s
193 when 'TrueClass'
193 when 'TrueClass'
194 l(:general_text_Yes)
194 l(:general_text_Yes)
195 when 'FalseClass'
195 when 'FalseClass'
196 l(:general_text_No)
196 l(:general_text_No)
197 when 'Issue'
197 when 'Issue'
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 when 'CustomValue', 'CustomFieldValue'
199 when 'CustomValue', 'CustomFieldValue'
200 if object.custom_field
200 if object.custom_field
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 if f.nil? || f.is_a?(String)
202 if f.nil? || f.is_a?(String)
203 f
203 f
204 else
204 else
205 format_object(f, html, &block)
205 format_object(f, html, &block)
206 end
206 end
207 else
207 else
208 object.value.to_s
208 object.value.to_s
209 end
209 end
210 else
210 else
211 html ? h(object) : object.to_s
211 html ? h(object) : object.to_s
212 end
212 end
213 end
213 end
214
214
215 def wiki_page_path(page, options={})
215 def wiki_page_path(page, options={})
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 end
217 end
218
218
219 def thumbnail_tag(attachment)
219 def thumbnail_tag(attachment)
220 link_to image_tag(thumbnail_path(attachment)),
220 link_to image_tag(thumbnail_path(attachment)),
221 named_attachment_path(attachment, attachment.filename),
221 named_attachment_path(attachment, attachment.filename),
222 :title => attachment.filename
222 :title => attachment.filename
223 end
223 end
224
224
225 def toggle_link(name, id, options={})
225 def toggle_link(name, id, options={})
226 onclick = "$('##{id}').toggle(); "
226 onclick = "$('##{id}').toggle(); "
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << "return false;"
228 onclick << "return false;"
229 link_to(name, "#", :onclick => onclick)
229 link_to(name, "#", :onclick => onclick)
230 end
230 end
231
231
232 def format_activity_title(text)
232 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
233 h(truncate_single_line_raw(text, 100))
234 end
234 end
235
235
236 def format_activity_day(date)
236 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
238 end
239
239
240 def format_activity_description(text)
240 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
243 end
244
244
245 def format_version_name(version)
245 def format_version_name(version)
246 if version.project == @project
246 if version.project == @project
247 h(version)
247 h(version)
248 else
248 else
249 h("#{version.project} - #{version}")
249 h("#{version.project} - #{version}")
250 end
250 end
251 end
251 end
252
252
253 def due_date_distance_in_words(date)
253 def due_date_distance_in_words(date)
254 if date
254 if date
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
255 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
256 end
256 end
257 end
257 end
258
258
259 # Renders a tree of projects as a nested set of unordered lists
259 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
260 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
261 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
262 def render_project_nested_lists(projects, &block)
263 s = ''
263 s = ''
264 if projects.any?
264 if projects.any?
265 ancestors = []
265 ancestors = []
266 original_project = @project
266 original_project = @project
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
268 # set the project environment to please macros.
269 @project = project
269 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
272 else
273 ancestors.pop
273 ancestors.pop
274 s << "</li>"
274 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
276 ancestors.pop
277 s << "</ul></li>\n"
277 s << "</ul></li>\n"
278 end
278 end
279 end
279 end
280 classes = (ancestors.empty? ? 'root' : 'child')
280 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
283 s << "</div>\n"
284 ancestors << project
284 ancestors << project
285 end
285 end
286 s << ("</li></ul>\n" * ancestors.size)
286 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
287 @project = original_project
288 end
288 end
289 s.html_safe
289 s.html_safe
290 end
290 end
291
291
292 def render_page_hierarchy(pages, node=nil, options={})
292 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
293 content = ''
294 if pages[node]
294 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
295 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
296 pages[node].each do |page|
297 content << "<li>"
297 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
301 content << "</li>\n"
302 end
302 end
303 content << "</ul>\n"
303 content << "</ul>\n"
304 end
304 end
305 content.html_safe
305 content.html_safe
306 end
306 end
307
307
308 # Renders flash messages
308 # Renders flash messages
309 def render_flash_messages
309 def render_flash_messages
310 s = ''
310 s = ''
311 flash.each do |k,v|
311 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
313 end
314 s.html_safe
314 s.html_safe
315 end
315 end
316
316
317 # Renders tabs and their content
317 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
318 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
319 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
320 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
321 selected = nil
322 end
322 end
323 selected ||= tabs.first[:name]
323 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
325 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
327 end
328 end
328 end
329
329
330 # Renders the project quick-jump box
330 # Renders the project quick-jump box
331 def render_project_jump_box
331 def render_project_jump_box
332 return unless User.current.logged?
332 return unless User.current.logged?
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 if projects.any?
334 if projects.any?
335 options =
335 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
337 '<option value="" disabled="disabled">---</option>').html_safe
338
338
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
341 end
342
342
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 end
345 end
346 end
346 end
347
347
348 def project_tree_options_for_select(projects, options = {})
348 def project_tree_options_for_select(projects, options = {})
349 s = ''.html_safe
349 s = ''.html_safe
350 if blank_text = options[:include_blank]
350 if blank_text = options[:include_blank]
351 if blank_text == true
351 if blank_text == true
352 blank_text = '&nbsp;'.html_safe
352 blank_text = '&nbsp;'.html_safe
353 end
353 end
354 s << content_tag('option', blank_text, :value => '')
354 s << content_tag('option', blank_text, :value => '')
355 end
355 end
356 project_tree(projects) do |project, level|
356 project_tree(projects) do |project, level|
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 tag_options = {:value => project.id}
358 tag_options = {:value => project.id}
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 tag_options[:selected] = 'selected'
360 tag_options[:selected] = 'selected'
361 else
361 else
362 tag_options[:selected] = nil
362 tag_options[:selected] = nil
363 end
363 end
364 tag_options.merge!(yield(project)) if block_given?
364 tag_options.merge!(yield(project)) if block_given?
365 s << content_tag('option', name_prefix + h(project), tag_options)
365 s << content_tag('option', name_prefix + h(project), tag_options)
366 end
366 end
367 s.html_safe
367 s.html_safe
368 end
368 end
369
369
370 # Yields the given block for each project with its level in the tree
370 # Yields the given block for each project with its level in the tree
371 #
371 #
372 # Wrapper for Project#project_tree
372 # Wrapper for Project#project_tree
373 def project_tree(projects, &block)
373 def project_tree(projects, &block)
374 Project.project_tree(projects, &block)
374 Project.project_tree(projects, &block)
375 end
375 end
376
376
377 def principals_check_box_tags(name, principals)
377 def principals_check_box_tags(name, principals)
378 s = ''
378 s = ''
379 principals.each do |principal|
379 principals.each do |principal|
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 end
381 end
382 s.html_safe
382 s.html_safe
383 end
383 end
384
384
385 # Returns a string for users/groups option tags
385 # Returns a string for users/groups option tags
386 def principals_options_for_select(collection, selected=nil)
386 def principals_options_for_select(collection, selected=nil)
387 s = ''
387 s = ''
388 if collection.include?(User.current)
388 if collection.include?(User.current)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 end
390 end
391 groups = ''
391 groups = ''
392 collection.sort.each do |element|
392 collection.sort.each do |element|
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 end
395 end
396 unless groups.empty?
396 unless groups.empty?
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 end
398 end
399 s.html_safe
399 s.html_safe
400 end
400 end
401
401
402 def option_tag(name, text, value, selected=nil, options={})
402 def option_tag(name, text, value, selected=nil, options={})
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 end
404 end
405
405
406 def truncate_single_line_raw(string, length)
406 def truncate_single_line_raw(string, length)
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 end
408 end
409
409
410 # Truncates at line break after 250 characters or options[:length]
410 # Truncates at line break after 250 characters or options[:length]
411 def truncate_lines(string, options={})
411 def truncate_lines(string, options={})
412 length = options[:length] || 250
412 length = options[:length] || 250
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 "#{$1}..."
414 "#{$1}..."
415 else
415 else
416 string
416 string
417 end
417 end
418 end
418 end
419
419
420 def anchor(text)
420 def anchor(text)
421 text.to_s.gsub(' ', '_')
421 text.to_s.gsub(' ', '_')
422 end
422 end
423
423
424 def html_hours(text)
424 def html_hours(text)
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 end
426 end
427
427
428 def authoring(created, author, options={})
428 def authoring(created, author, options={})
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 end
430 end
431
431
432 def time_tag(time)
432 def time_tag(time)
433 text = distance_of_time_in_words(Time.now, time)
433 text = distance_of_time_in_words(Time.now, time)
434 if @project
434 if @project
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 else
436 else
437 content_tag('abbr', text, :title => format_time(time))
437 content_tag('abbr', text, :title => format_time(time))
438 end
438 end
439 end
439 end
440
440
441 def syntax_highlight_lines(name, content)
441 def syntax_highlight_lines(name, content)
442 lines = []
442 lines = []
443 syntax_highlight(name, content).each_line { |line| lines << line }
443 syntax_highlight(name, content).each_line { |line| lines << line }
444 lines
444 lines
445 end
445 end
446
446
447 def syntax_highlight(name, content)
447 def syntax_highlight(name, content)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 end
449 end
450
450
451 def to_path_param(path)
451 def to_path_param(path)
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str.blank? ? nil : str
453 str.blank? ? nil : str
454 end
454 end
455
455
456 def reorder_links(name, url, method = :post)
456 def reorder_links(name, url, method = :post)
457 link_to(l(:label_sort_highest),
457 link_to(l(:label_sort_highest),
458 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
458 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
459 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
459 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
460 link_to(l(:label_sort_higher),
460 link_to(l(:label_sort_higher),
461 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
461 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
462 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
462 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
463 link_to(l(:label_sort_lower),
463 link_to(l(:label_sort_lower),
464 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
464 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
465 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
465 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
466 link_to(l(:label_sort_lowest),
466 link_to(l(:label_sort_lowest),
467 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
467 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
468 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
468 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
469 end
469 end
470
470
471 def reorder_handle(object, options={})
472 data = {
473 :reorder_url => options[:url] || url_for(object),
474 :reorder_param => options[:param] || object.class.name.underscore
475 }
476 content_tag('span', '',
477 :class => "sort-handle ui-icon ui-icon-arrowthick-2-n-s",
478 :data => data)
479 end
480
471 def breadcrumb(*args)
481 def breadcrumb(*args)
472 elements = args.flatten
482 elements = args.flatten
473 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
483 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
474 end
484 end
475
485
476 def other_formats_links(&block)
486 def other_formats_links(&block)
477 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
487 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
478 yield Redmine::Views::OtherFormatsBuilder.new(self)
488 yield Redmine::Views::OtherFormatsBuilder.new(self)
479 concat('</p>'.html_safe)
489 concat('</p>'.html_safe)
480 end
490 end
481
491
482 def page_header_title
492 def page_header_title
483 if @project.nil? || @project.new_record?
493 if @project.nil? || @project.new_record?
484 h(Setting.app_title)
494 h(Setting.app_title)
485 else
495 else
486 b = []
496 b = []
487 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
497 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
488 if ancestors.any?
498 if ancestors.any?
489 root = ancestors.shift
499 root = ancestors.shift
490 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
500 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
491 if ancestors.size > 2
501 if ancestors.size > 2
492 b << "\xe2\x80\xa6"
502 b << "\xe2\x80\xa6"
493 ancestors = ancestors[-2, 2]
503 ancestors = ancestors[-2, 2]
494 end
504 end
495 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
505 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
496 end
506 end
497 b << content_tag(:span, h(@project), class: 'current-project')
507 b << content_tag(:span, h(@project), class: 'current-project')
498 if b.size > 1
508 if b.size > 1
499 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
509 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
500 path = safe_join(b[0..-2], separator) + separator
510 path = safe_join(b[0..-2], separator) + separator
501 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
511 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
502 end
512 end
503 safe_join b
513 safe_join b
504 end
514 end
505 end
515 end
506
516
507 # Returns a h2 tag and sets the html title with the given arguments
517 # Returns a h2 tag and sets the html title with the given arguments
508 def title(*args)
518 def title(*args)
509 strings = args.map do |arg|
519 strings = args.map do |arg|
510 if arg.is_a?(Array) && arg.size >= 2
520 if arg.is_a?(Array) && arg.size >= 2
511 link_to(*arg)
521 link_to(*arg)
512 else
522 else
513 h(arg.to_s)
523 h(arg.to_s)
514 end
524 end
515 end
525 end
516 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
526 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
517 content_tag('h2', strings.join(' &#187; ').html_safe)
527 content_tag('h2', strings.join(' &#187; ').html_safe)
518 end
528 end
519
529
520 # Sets the html title
530 # Sets the html title
521 # Returns the html title when called without arguments
531 # Returns the html title when called without arguments
522 # Current project name and app_title and automatically appended
532 # Current project name and app_title and automatically appended
523 # Exemples:
533 # Exemples:
524 # html_title 'Foo', 'Bar'
534 # html_title 'Foo', 'Bar'
525 # html_title # => 'Foo - Bar - My Project - Redmine'
535 # html_title # => 'Foo - Bar - My Project - Redmine'
526 def html_title(*args)
536 def html_title(*args)
527 if args.empty?
537 if args.empty?
528 title = @html_title || []
538 title = @html_title || []
529 title << @project.name if @project
539 title << @project.name if @project
530 title << Setting.app_title unless Setting.app_title == title.last
540 title << Setting.app_title unless Setting.app_title == title.last
531 title.reject(&:blank?).join(' - ')
541 title.reject(&:blank?).join(' - ')
532 else
542 else
533 @html_title ||= []
543 @html_title ||= []
534 @html_title += args
544 @html_title += args
535 end
545 end
536 end
546 end
537
547
538 # Returns the theme, controller name, and action as css classes for the
548 # Returns the theme, controller name, and action as css classes for the
539 # HTML body.
549 # HTML body.
540 def body_css_classes
550 def body_css_classes
541 css = []
551 css = []
542 if theme = Redmine::Themes.theme(Setting.ui_theme)
552 if theme = Redmine::Themes.theme(Setting.ui_theme)
543 css << 'theme-' + theme.name
553 css << 'theme-' + theme.name
544 end
554 end
545
555
546 css << 'project-' + @project.identifier if @project && @project.identifier.present?
556 css << 'project-' + @project.identifier if @project && @project.identifier.present?
547 css << 'controller-' + controller_name
557 css << 'controller-' + controller_name
548 css << 'action-' + action_name
558 css << 'action-' + action_name
549 css.join(' ')
559 css.join(' ')
550 end
560 end
551
561
552 def accesskey(s)
562 def accesskey(s)
553 @used_accesskeys ||= []
563 @used_accesskeys ||= []
554 key = Redmine::AccessKeys.key_for(s)
564 key = Redmine::AccessKeys.key_for(s)
555 return nil if @used_accesskeys.include?(key)
565 return nil if @used_accesskeys.include?(key)
556 @used_accesskeys << key
566 @used_accesskeys << key
557 key
567 key
558 end
568 end
559
569
560 # Formats text according to system settings.
570 # Formats text according to system settings.
561 # 2 ways to call this method:
571 # 2 ways to call this method:
562 # * with a String: textilizable(text, options)
572 # * with a String: textilizable(text, options)
563 # * with an object and one of its attribute: textilizable(issue, :description, options)
573 # * with an object and one of its attribute: textilizable(issue, :description, options)
564 def textilizable(*args)
574 def textilizable(*args)
565 options = args.last.is_a?(Hash) ? args.pop : {}
575 options = args.last.is_a?(Hash) ? args.pop : {}
566 case args.size
576 case args.size
567 when 1
577 when 1
568 obj = options[:object]
578 obj = options[:object]
569 text = args.shift
579 text = args.shift
570 when 2
580 when 2
571 obj = args.shift
581 obj = args.shift
572 attr = args.shift
582 attr = args.shift
573 text = obj.send(attr).to_s
583 text = obj.send(attr).to_s
574 else
584 else
575 raise ArgumentError, 'invalid arguments to textilizable'
585 raise ArgumentError, 'invalid arguments to textilizable'
576 end
586 end
577 return '' if text.blank?
587 return '' if text.blank?
578 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
588 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
579 @only_path = only_path = options.delete(:only_path) == false ? false : true
589 @only_path = only_path = options.delete(:only_path) == false ? false : true
580
590
581 text = text.dup
591 text = text.dup
582 macros = catch_macros(text)
592 macros = catch_macros(text)
583 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
593 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
584
594
585 @parsed_headings = []
595 @parsed_headings = []
586 @heading_anchors = {}
596 @heading_anchors = {}
587 @current_section = 0 if options[:edit_section_links]
597 @current_section = 0 if options[:edit_section_links]
588
598
589 parse_sections(text, project, obj, attr, only_path, options)
599 parse_sections(text, project, obj, attr, only_path, options)
590 text = parse_non_pre_blocks(text, obj, macros) do |text|
600 text = parse_non_pre_blocks(text, obj, macros) do |text|
591 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
601 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
592 send method_name, text, project, obj, attr, only_path, options
602 send method_name, text, project, obj, attr, only_path, options
593 end
603 end
594 end
604 end
595 parse_headings(text, project, obj, attr, only_path, options)
605 parse_headings(text, project, obj, attr, only_path, options)
596
606
597 if @parsed_headings.any?
607 if @parsed_headings.any?
598 replace_toc(text, @parsed_headings)
608 replace_toc(text, @parsed_headings)
599 end
609 end
600
610
601 text.html_safe
611 text.html_safe
602 end
612 end
603
613
604 def parse_non_pre_blocks(text, obj, macros)
614 def parse_non_pre_blocks(text, obj, macros)
605 s = StringScanner.new(text)
615 s = StringScanner.new(text)
606 tags = []
616 tags = []
607 parsed = ''
617 parsed = ''
608 while !s.eos?
618 while !s.eos?
609 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
619 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
610 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
620 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
611 if tags.empty?
621 if tags.empty?
612 yield text
622 yield text
613 inject_macros(text, obj, macros) if macros.any?
623 inject_macros(text, obj, macros) if macros.any?
614 else
624 else
615 inject_macros(text, obj, macros, false) if macros.any?
625 inject_macros(text, obj, macros, false) if macros.any?
616 end
626 end
617 parsed << text
627 parsed << text
618 if tag
628 if tag
619 if closing
629 if closing
620 if tags.last && tags.last.casecmp(tag) == 0
630 if tags.last && tags.last.casecmp(tag) == 0
621 tags.pop
631 tags.pop
622 end
632 end
623 else
633 else
624 tags << tag.downcase
634 tags << tag.downcase
625 end
635 end
626 parsed << full_tag
636 parsed << full_tag
627 end
637 end
628 end
638 end
629 # Close any non closing tags
639 # Close any non closing tags
630 while tag = tags.pop
640 while tag = tags.pop
631 parsed << "</#{tag}>"
641 parsed << "</#{tag}>"
632 end
642 end
633 parsed
643 parsed
634 end
644 end
635
645
636 def parse_inline_attachments(text, project, obj, attr, only_path, options)
646 def parse_inline_attachments(text, project, obj, attr, only_path, options)
637 return if options[:inline_attachments] == false
647 return if options[:inline_attachments] == false
638
648
639 # when using an image link, try to use an attachment, if possible
649 # when using an image link, try to use an attachment, if possible
640 attachments = options[:attachments] || []
650 attachments = options[:attachments] || []
641 attachments += obj.attachments if obj.respond_to?(:attachments)
651 attachments += obj.attachments if obj.respond_to?(:attachments)
642 if attachments.present?
652 if attachments.present?
643 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
653 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
644 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
654 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
645 # search for the picture in attachments
655 # search for the picture in attachments
646 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
656 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
647 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
657 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
648 desc = found.description.to_s.gsub('"', '')
658 desc = found.description.to_s.gsub('"', '')
649 if !desc.blank? && alttext.blank?
659 if !desc.blank? && alttext.blank?
650 alt = " title=\"#{desc}\" alt=\"#{desc}\""
660 alt = " title=\"#{desc}\" alt=\"#{desc}\""
651 end
661 end
652 "src=\"#{image_url}\"#{alt}"
662 "src=\"#{image_url}\"#{alt}"
653 else
663 else
654 m
664 m
655 end
665 end
656 end
666 end
657 end
667 end
658 end
668 end
659
669
660 # Wiki links
670 # Wiki links
661 #
671 #
662 # Examples:
672 # Examples:
663 # [[mypage]]
673 # [[mypage]]
664 # [[mypage|mytext]]
674 # [[mypage|mytext]]
665 # wiki links can refer other project wikis, using project name or identifier:
675 # wiki links can refer other project wikis, using project name or identifier:
666 # [[project:]] -> wiki starting page
676 # [[project:]] -> wiki starting page
667 # [[project:|mytext]]
677 # [[project:|mytext]]
668 # [[project:mypage]]
678 # [[project:mypage]]
669 # [[project:mypage|mytext]]
679 # [[project:mypage|mytext]]
670 def parse_wiki_links(text, project, obj, attr, only_path, options)
680 def parse_wiki_links(text, project, obj, attr, only_path, options)
671 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
681 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
672 link_project = project
682 link_project = project
673 esc, all, page, title = $1, $2, $3, $5
683 esc, all, page, title = $1, $2, $3, $5
674 if esc.nil?
684 if esc.nil?
675 if page =~ /^([^\:]+)\:(.*)$/
685 if page =~ /^([^\:]+)\:(.*)$/
676 identifier, page = $1, $2
686 identifier, page = $1, $2
677 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
687 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
678 title ||= identifier if page.blank?
688 title ||= identifier if page.blank?
679 end
689 end
680
690
681 if link_project && link_project.wiki
691 if link_project && link_project.wiki
682 # extract anchor
692 # extract anchor
683 anchor = nil
693 anchor = nil
684 if page =~ /^(.+?)\#(.+)$/
694 if page =~ /^(.+?)\#(.+)$/
685 page, anchor = $1, $2
695 page, anchor = $1, $2
686 end
696 end
687 anchor = sanitize_anchor_name(anchor) if anchor.present?
697 anchor = sanitize_anchor_name(anchor) if anchor.present?
688 # check if page exists
698 # check if page exists
689 wiki_page = link_project.wiki.find_page(page)
699 wiki_page = link_project.wiki.find_page(page)
690 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
700 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
691 "##{anchor}"
701 "##{anchor}"
692 else
702 else
693 case options[:wiki_links]
703 case options[:wiki_links]
694 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
704 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
695 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
705 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
696 else
706 else
697 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
707 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
698 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
708 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
699 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
709 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
700 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
710 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
701 end
711 end
702 end
712 end
703 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
713 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
704 else
714 else
705 # project or wiki doesn't exist
715 # project or wiki doesn't exist
706 all
716 all
707 end
717 end
708 else
718 else
709 all
719 all
710 end
720 end
711 end
721 end
712 end
722 end
713
723
714 # Redmine links
724 # Redmine links
715 #
725 #
716 # Examples:
726 # Examples:
717 # Issues:
727 # Issues:
718 # #52 -> Link to issue #52
728 # #52 -> Link to issue #52
719 # Changesets:
729 # Changesets:
720 # r52 -> Link to revision 52
730 # r52 -> Link to revision 52
721 # commit:a85130f -> Link to scmid starting with a85130f
731 # commit:a85130f -> Link to scmid starting with a85130f
722 # Documents:
732 # Documents:
723 # document#17 -> Link to document with id 17
733 # document#17 -> Link to document with id 17
724 # document:Greetings -> Link to the document with title "Greetings"
734 # document:Greetings -> Link to the document with title "Greetings"
725 # document:"Some document" -> Link to the document with title "Some document"
735 # document:"Some document" -> Link to the document with title "Some document"
726 # Versions:
736 # Versions:
727 # version#3 -> Link to version with id 3
737 # version#3 -> Link to version with id 3
728 # version:1.0.0 -> Link to version named "1.0.0"
738 # version:1.0.0 -> Link to version named "1.0.0"
729 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
739 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
730 # Attachments:
740 # Attachments:
731 # attachment:file.zip -> Link to the attachment of the current object named file.zip
741 # attachment:file.zip -> Link to the attachment of the current object named file.zip
732 # Source files:
742 # Source files:
733 # source:some/file -> Link to the file located at /some/file in the project's repository
743 # source:some/file -> Link to the file located at /some/file in the project's repository
734 # source:some/file@52 -> Link to the file's revision 52
744 # source:some/file@52 -> Link to the file's revision 52
735 # source:some/file#L120 -> Link to line 120 of the file
745 # source:some/file#L120 -> Link to line 120 of the file
736 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
746 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
737 # export:some/file -> Force the download of the file
747 # export:some/file -> Force the download of the file
738 # Forum messages:
748 # Forum messages:
739 # message#1218 -> Link to message with id 1218
749 # message#1218 -> Link to message with id 1218
740 # Projects:
750 # Projects:
741 # project:someproject -> Link to project named "someproject"
751 # project:someproject -> Link to project named "someproject"
742 # project#3 -> Link to project with id 3
752 # project#3 -> Link to project with id 3
743 #
753 #
744 # Links can refer other objects from other projects, using project identifier:
754 # Links can refer other objects from other projects, using project identifier:
745 # identifier:r52
755 # identifier:r52
746 # identifier:document:"Some document"
756 # identifier:document:"Some document"
747 # identifier:version:1.0.0
757 # identifier:version:1.0.0
748 # identifier:source:some/file
758 # identifier:source:some/file
749 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
759 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
750 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
760 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
751 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
761 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
752 if tag_content
762 if tag_content
753 $&
763 $&
754 else
764 else
755 link = nil
765 link = nil
756 project = default_project
766 project = default_project
757 if project_identifier
767 if project_identifier
758 project = Project.visible.find_by_identifier(project_identifier)
768 project = Project.visible.find_by_identifier(project_identifier)
759 end
769 end
760 if esc.nil?
770 if esc.nil?
761 if prefix.nil? && sep == 'r'
771 if prefix.nil? && sep == 'r'
762 if project
772 if project
763 repository = nil
773 repository = nil
764 if repo_identifier
774 if repo_identifier
765 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
775 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
766 else
776 else
767 repository = project.repository
777 repository = project.repository
768 end
778 end
769 # project.changesets.visible raises an SQL error because of a double join on repositories
779 # project.changesets.visible raises an SQL error because of a double join on repositories
770 if repository &&
780 if repository &&
771 (changeset = Changeset.visible.
781 (changeset = Changeset.visible.
772 find_by_repository_id_and_revision(repository.id, identifier))
782 find_by_repository_id_and_revision(repository.id, identifier))
773 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
783 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
774 {:only_path => only_path, :controller => 'repositories',
784 {:only_path => only_path, :controller => 'repositories',
775 :action => 'revision', :id => project,
785 :action => 'revision', :id => project,
776 :repository_id => repository.identifier_param,
786 :repository_id => repository.identifier_param,
777 :rev => changeset.revision},
787 :rev => changeset.revision},
778 :class => 'changeset',
788 :class => 'changeset',
779 :title => truncate_single_line_raw(changeset.comments, 100))
789 :title => truncate_single_line_raw(changeset.comments, 100))
780 end
790 end
781 end
791 end
782 elsif sep == '#'
792 elsif sep == '#'
783 oid = identifier.to_i
793 oid = identifier.to_i
784 case prefix
794 case prefix
785 when nil
795 when nil
786 if oid.to_s == identifier &&
796 if oid.to_s == identifier &&
787 issue = Issue.visible.find_by_id(oid)
797 issue = Issue.visible.find_by_id(oid)
788 anchor = comment_id ? "note-#{comment_id}" : nil
798 anchor = comment_id ? "note-#{comment_id}" : nil
789 link = link_to("##{oid}#{comment_suffix}",
799 link = link_to("##{oid}#{comment_suffix}",
790 issue_url(issue, :only_path => only_path, :anchor => anchor),
800 issue_url(issue, :only_path => only_path, :anchor => anchor),
791 :class => issue.css_classes,
801 :class => issue.css_classes,
792 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
802 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
793 end
803 end
794 when 'document'
804 when 'document'
795 if document = Document.visible.find_by_id(oid)
805 if document = Document.visible.find_by_id(oid)
796 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
806 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
797 end
807 end
798 when 'version'
808 when 'version'
799 if version = Version.visible.find_by_id(oid)
809 if version = Version.visible.find_by_id(oid)
800 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
810 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
801 end
811 end
802 when 'message'
812 when 'message'
803 if message = Message.visible.find_by_id(oid)
813 if message = Message.visible.find_by_id(oid)
804 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
814 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
805 end
815 end
806 when 'forum'
816 when 'forum'
807 if board = Board.visible.find_by_id(oid)
817 if board = Board.visible.find_by_id(oid)
808 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
818 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
809 end
819 end
810 when 'news'
820 when 'news'
811 if news = News.visible.find_by_id(oid)
821 if news = News.visible.find_by_id(oid)
812 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
822 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
813 end
823 end
814 when 'project'
824 when 'project'
815 if p = Project.visible.find_by_id(oid)
825 if p = Project.visible.find_by_id(oid)
816 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
826 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 end
827 end
818 end
828 end
819 elsif sep == ':'
829 elsif sep == ':'
820 # removes the double quotes if any
830 # removes the double quotes if any
821 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
831 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
822 name = CGI.unescapeHTML(name)
832 name = CGI.unescapeHTML(name)
823 case prefix
833 case prefix
824 when 'document'
834 when 'document'
825 if project && document = project.documents.visible.find_by_title(name)
835 if project && document = project.documents.visible.find_by_title(name)
826 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
836 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
827 end
837 end
828 when 'version'
838 when 'version'
829 if project && version = project.versions.visible.find_by_name(name)
839 if project && version = project.versions.visible.find_by_name(name)
830 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
840 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
831 end
841 end
832 when 'forum'
842 when 'forum'
833 if project && board = project.boards.visible.find_by_name(name)
843 if project && board = project.boards.visible.find_by_name(name)
834 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
844 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
835 end
845 end
836 when 'news'
846 when 'news'
837 if project && news = project.news.visible.find_by_title(name)
847 if project && news = project.news.visible.find_by_title(name)
838 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
848 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
839 end
849 end
840 when 'commit', 'source', 'export'
850 when 'commit', 'source', 'export'
841 if project
851 if project
842 repository = nil
852 repository = nil
843 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
853 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
844 repo_prefix, repo_identifier, name = $1, $2, $3
854 repo_prefix, repo_identifier, name = $1, $2, $3
845 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
855 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
846 else
856 else
847 repository = project.repository
857 repository = project.repository
848 end
858 end
849 if prefix == 'commit'
859 if prefix == 'commit'
850 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
860 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
851 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
861 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
852 :class => 'changeset',
862 :class => 'changeset',
853 :title => truncate_single_line_raw(changeset.comments, 100)
863 :title => truncate_single_line_raw(changeset.comments, 100)
854 end
864 end
855 else
865 else
856 if repository && User.current.allowed_to?(:browse_repository, project)
866 if repository && User.current.allowed_to?(:browse_repository, project)
857 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
867 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
858 path, rev, anchor = $1, $3, $5
868 path, rev, anchor = $1, $3, $5
859 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
869 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
860 :path => to_path_param(path),
870 :path => to_path_param(path),
861 :rev => rev,
871 :rev => rev,
862 :anchor => anchor},
872 :anchor => anchor},
863 :class => (prefix == 'export' ? 'source download' : 'source')
873 :class => (prefix == 'export' ? 'source download' : 'source')
864 end
874 end
865 end
875 end
866 repo_prefix = nil
876 repo_prefix = nil
867 end
877 end
868 when 'attachment'
878 when 'attachment'
869 attachments = options[:attachments] || []
879 attachments = options[:attachments] || []
870 attachments += obj.attachments if obj.respond_to?(:attachments)
880 attachments += obj.attachments if obj.respond_to?(:attachments)
871 if attachments && attachment = Attachment.latest_attach(attachments, name)
881 if attachments && attachment = Attachment.latest_attach(attachments, name)
872 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
882 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
873 end
883 end
874 when 'project'
884 when 'project'
875 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
885 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
876 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
886 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
877 end
887 end
878 end
888 end
879 end
889 end
880 end
890 end
881 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
891 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
882 end
892 end
883 end
893 end
884 end
894 end
885
895
886 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
896 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
887
897
888 def parse_sections(text, project, obj, attr, only_path, options)
898 def parse_sections(text, project, obj, attr, only_path, options)
889 return unless options[:edit_section_links]
899 return unless options[:edit_section_links]
890 text.gsub!(HEADING_RE) do
900 text.gsub!(HEADING_RE) do
891 heading, level = $1, $2
901 heading, level = $1, $2
892 @current_section += 1
902 @current_section += 1
893 if @current_section > 1
903 if @current_section > 1
894 content_tag('div',
904 content_tag('div',
895 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
905 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
896 :class => 'icon-only icon-edit'),
906 :class => 'icon-only icon-edit'),
897 :class => "contextual heading-#{level}",
907 :class => "contextual heading-#{level}",
898 :title => l(:button_edit_section),
908 :title => l(:button_edit_section),
899 :id => "section-#{@current_section}") + heading.html_safe
909 :id => "section-#{@current_section}") + heading.html_safe
900 else
910 else
901 heading
911 heading
902 end
912 end
903 end
913 end
904 end
914 end
905
915
906 # Headings and TOC
916 # Headings and TOC
907 # Adds ids and links to headings unless options[:headings] is set to false
917 # Adds ids and links to headings unless options[:headings] is set to false
908 def parse_headings(text, project, obj, attr, only_path, options)
918 def parse_headings(text, project, obj, attr, only_path, options)
909 return if options[:headings] == false
919 return if options[:headings] == false
910
920
911 text.gsub!(HEADING_RE) do
921 text.gsub!(HEADING_RE) do
912 level, attrs, content = $2.to_i, $3, $4
922 level, attrs, content = $2.to_i, $3, $4
913 item = strip_tags(content).strip
923 item = strip_tags(content).strip
914 anchor = sanitize_anchor_name(item)
924 anchor = sanitize_anchor_name(item)
915 # used for single-file wiki export
925 # used for single-file wiki export
916 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
926 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
917 @heading_anchors[anchor] ||= 0
927 @heading_anchors[anchor] ||= 0
918 idx = (@heading_anchors[anchor] += 1)
928 idx = (@heading_anchors[anchor] += 1)
919 if idx > 1
929 if idx > 1
920 anchor = "#{anchor}-#{idx}"
930 anchor = "#{anchor}-#{idx}"
921 end
931 end
922 @parsed_headings << [level, anchor, item]
932 @parsed_headings << [level, anchor, item]
923 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
933 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
924 end
934 end
925 end
935 end
926
936
927 MACROS_RE = /(
937 MACROS_RE = /(
928 (!)? # escaping
938 (!)? # escaping
929 (
939 (
930 \{\{ # opening tag
940 \{\{ # opening tag
931 ([\w]+) # macro name
941 ([\w]+) # macro name
932 (\(([^\n\r]*?)\))? # optional arguments
942 (\(([^\n\r]*?)\))? # optional arguments
933 ([\n\r].*?[\n\r])? # optional block of text
943 ([\n\r].*?[\n\r])? # optional block of text
934 \}\} # closing tag
944 \}\} # closing tag
935 )
945 )
936 )/mx unless const_defined?(:MACROS_RE)
946 )/mx unless const_defined?(:MACROS_RE)
937
947
938 MACRO_SUB_RE = /(
948 MACRO_SUB_RE = /(
939 \{\{
949 \{\{
940 macro\((\d+)\)
950 macro\((\d+)\)
941 \}\}
951 \}\}
942 )/x unless const_defined?(:MACRO_SUB_RE)
952 )/x unless const_defined?(:MACRO_SUB_RE)
943
953
944 # Extracts macros from text
954 # Extracts macros from text
945 def catch_macros(text)
955 def catch_macros(text)
946 macros = {}
956 macros = {}
947 text.gsub!(MACROS_RE) do
957 text.gsub!(MACROS_RE) do
948 all, macro = $1, $4.downcase
958 all, macro = $1, $4.downcase
949 if macro_exists?(macro) || all =~ MACRO_SUB_RE
959 if macro_exists?(macro) || all =~ MACRO_SUB_RE
950 index = macros.size
960 index = macros.size
951 macros[index] = all
961 macros[index] = all
952 "{{macro(#{index})}}"
962 "{{macro(#{index})}}"
953 else
963 else
954 all
964 all
955 end
965 end
956 end
966 end
957 macros
967 macros
958 end
968 end
959
969
960 # Executes and replaces macros in text
970 # Executes and replaces macros in text
961 def inject_macros(text, obj, macros, execute=true)
971 def inject_macros(text, obj, macros, execute=true)
962 text.gsub!(MACRO_SUB_RE) do
972 text.gsub!(MACRO_SUB_RE) do
963 all, index = $1, $2.to_i
973 all, index = $1, $2.to_i
964 orig = macros.delete(index)
974 orig = macros.delete(index)
965 if execute && orig && orig =~ MACROS_RE
975 if execute && orig && orig =~ MACROS_RE
966 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
976 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
967 if esc.nil?
977 if esc.nil?
968 h(exec_macro(macro, obj, args, block) || all)
978 h(exec_macro(macro, obj, args, block) || all)
969 else
979 else
970 h(all)
980 h(all)
971 end
981 end
972 elsif orig
982 elsif orig
973 h(orig)
983 h(orig)
974 else
984 else
975 h(all)
985 h(all)
976 end
986 end
977 end
987 end
978 end
988 end
979
989
980 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
990 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
981
991
982 # Renders the TOC with given headings
992 # Renders the TOC with given headings
983 def replace_toc(text, headings)
993 def replace_toc(text, headings)
984 text.gsub!(TOC_RE) do
994 text.gsub!(TOC_RE) do
985 left_align, right_align = $2, $3
995 left_align, right_align = $2, $3
986 # Keep only the 4 first levels
996 # Keep only the 4 first levels
987 headings = headings.select{|level, anchor, item| level <= 4}
997 headings = headings.select{|level, anchor, item| level <= 4}
988 if headings.empty?
998 if headings.empty?
989 ''
999 ''
990 else
1000 else
991 div_class = 'toc'
1001 div_class = 'toc'
992 div_class << ' right' if right_align
1002 div_class << ' right' if right_align
993 div_class << ' left' if left_align
1003 div_class << ' left' if left_align
994 out = "<ul class=\"#{div_class}\"><li>"
1004 out = "<ul class=\"#{div_class}\"><li>"
995 root = headings.map(&:first).min
1005 root = headings.map(&:first).min
996 current = root
1006 current = root
997 started = false
1007 started = false
998 headings.each do |level, anchor, item|
1008 headings.each do |level, anchor, item|
999 if level > current
1009 if level > current
1000 out << '<ul><li>' * (level - current)
1010 out << '<ul><li>' * (level - current)
1001 elsif level < current
1011 elsif level < current
1002 out << "</li></ul>\n" * (current - level) + "</li><li>"
1012 out << "</li></ul>\n" * (current - level) + "</li><li>"
1003 elsif started
1013 elsif started
1004 out << '</li><li>'
1014 out << '</li><li>'
1005 end
1015 end
1006 out << "<a href=\"##{anchor}\">#{item}</a>"
1016 out << "<a href=\"##{anchor}\">#{item}</a>"
1007 current = level
1017 current = level
1008 started = true
1018 started = true
1009 end
1019 end
1010 out << '</li></ul>' * (current - root)
1020 out << '</li></ul>' * (current - root)
1011 out << '</li></ul>'
1021 out << '</li></ul>'
1012 end
1022 end
1013 end
1023 end
1014 end
1024 end
1015
1025
1016 # Same as Rails' simple_format helper without using paragraphs
1026 # Same as Rails' simple_format helper without using paragraphs
1017 def simple_format_without_paragraph(text)
1027 def simple_format_without_paragraph(text)
1018 text.to_s.
1028 text.to_s.
1019 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1029 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1020 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1030 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1021 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1031 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1022 html_safe
1032 html_safe
1023 end
1033 end
1024
1034
1025 def lang_options_for_select(blank=true)
1035 def lang_options_for_select(blank=true)
1026 (blank ? [["(auto)", ""]] : []) + languages_options
1036 (blank ? [["(auto)", ""]] : []) + languages_options
1027 end
1037 end
1028
1038
1029 def labelled_form_for(*args, &proc)
1039 def labelled_form_for(*args, &proc)
1030 args << {} unless args.last.is_a?(Hash)
1040 args << {} unless args.last.is_a?(Hash)
1031 options = args.last
1041 options = args.last
1032 if args.first.is_a?(Symbol)
1042 if args.first.is_a?(Symbol)
1033 options.merge!(:as => args.shift)
1043 options.merge!(:as => args.shift)
1034 end
1044 end
1035 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1045 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1036 form_for(*args, &proc)
1046 form_for(*args, &proc)
1037 end
1047 end
1038
1048
1039 def labelled_fields_for(*args, &proc)
1049 def labelled_fields_for(*args, &proc)
1040 args << {} unless args.last.is_a?(Hash)
1050 args << {} unless args.last.is_a?(Hash)
1041 options = args.last
1051 options = args.last
1042 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1052 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1043 fields_for(*args, &proc)
1053 fields_for(*args, &proc)
1044 end
1054 end
1045
1055
1046 def error_messages_for(*objects)
1056 def error_messages_for(*objects)
1047 html = ""
1057 html = ""
1048 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1058 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1049 errors = objects.map {|o| o.errors.full_messages}.flatten
1059 errors = objects.map {|o| o.errors.full_messages}.flatten
1050 if errors.any?
1060 if errors.any?
1051 html << "<div id='errorExplanation'><ul>\n"
1061 html << "<div id='errorExplanation'><ul>\n"
1052 errors.each do |error|
1062 errors.each do |error|
1053 html << "<li>#{h error}</li>\n"
1063 html << "<li>#{h error}</li>\n"
1054 end
1064 end
1055 html << "</ul></div>\n"
1065 html << "</ul></div>\n"
1056 end
1066 end
1057 html.html_safe
1067 html.html_safe
1058 end
1068 end
1059
1069
1060 def delete_link(url, options={})
1070 def delete_link(url, options={})
1061 options = {
1071 options = {
1062 :method => :delete,
1072 :method => :delete,
1063 :data => {:confirm => l(:text_are_you_sure)},
1073 :data => {:confirm => l(:text_are_you_sure)},
1064 :class => 'icon icon-del'
1074 :class => 'icon icon-del'
1065 }.merge(options)
1075 }.merge(options)
1066
1076
1067 link_to l(:button_delete), url, options
1077 link_to l(:button_delete), url, options
1068 end
1078 end
1069
1079
1070 def preview_link(url, form, target='preview', options={})
1080 def preview_link(url, form, target='preview', options={})
1071 content_tag 'a', l(:label_preview), {
1081 content_tag 'a', l(:label_preview), {
1072 :href => "#",
1082 :href => "#",
1073 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1083 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1074 :accesskey => accesskey(:preview)
1084 :accesskey => accesskey(:preview)
1075 }.merge(options)
1085 }.merge(options)
1076 end
1086 end
1077
1087
1078 def link_to_function(name, function, html_options={})
1088 def link_to_function(name, function, html_options={})
1079 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1089 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1080 end
1090 end
1081
1091
1082 # Helper to render JSON in views
1092 # Helper to render JSON in views
1083 def raw_json(arg)
1093 def raw_json(arg)
1084 arg.to_json.to_s.gsub('/', '\/').html_safe
1094 arg.to_json.to_s.gsub('/', '\/').html_safe
1085 end
1095 end
1086
1096
1087 def back_url
1097 def back_url
1088 url = params[:back_url]
1098 url = params[:back_url]
1089 if url.nil? && referer = request.env['HTTP_REFERER']
1099 if url.nil? && referer = request.env['HTTP_REFERER']
1090 url = CGI.unescape(referer.to_s)
1100 url = CGI.unescape(referer.to_s)
1091 end
1101 end
1092 url
1102 url
1093 end
1103 end
1094
1104
1095 def back_url_hidden_field_tag
1105 def back_url_hidden_field_tag
1096 url = back_url
1106 url = back_url
1097 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1107 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1098 end
1108 end
1099
1109
1100 def check_all_links(form_name)
1110 def check_all_links(form_name)
1101 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1111 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1102 " | ".html_safe +
1112 " | ".html_safe +
1103 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1113 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1104 end
1114 end
1105
1115
1106 def toggle_checkboxes_link(selector)
1116 def toggle_checkboxes_link(selector)
1107 link_to_function '',
1117 link_to_function '',
1108 "toggleCheckboxesBySelector('#{selector}')",
1118 "toggleCheckboxesBySelector('#{selector}')",
1109 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1119 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1110 :class => 'toggle-checkboxes'
1120 :class => 'toggle-checkboxes'
1111 end
1121 end
1112
1122
1113 def progress_bar(pcts, options={})
1123 def progress_bar(pcts, options={})
1114 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1124 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1115 pcts = pcts.collect(&:round)
1125 pcts = pcts.collect(&:round)
1116 pcts[1] = pcts[1] - pcts[0]
1126 pcts[1] = pcts[1] - pcts[0]
1117 pcts << (100 - pcts[1] - pcts[0])
1127 pcts << (100 - pcts[1] - pcts[0])
1118 titles = options[:titles].to_a
1128 titles = options[:titles].to_a
1119 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1129 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1120 legend = options[:legend] || ''
1130 legend = options[:legend] || ''
1121 content_tag('table',
1131 content_tag('table',
1122 content_tag('tr',
1132 content_tag('tr',
1123 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1133 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1124 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1134 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1125 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1135 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1126 ), :class => "progress progress-#{pcts[0]}").html_safe +
1136 ), :class => "progress progress-#{pcts[0]}").html_safe +
1127 content_tag('p', legend, :class => 'percent').html_safe
1137 content_tag('p', legend, :class => 'percent').html_safe
1128 end
1138 end
1129
1139
1130 def checked_image(checked=true)
1140 def checked_image(checked=true)
1131 if checked
1141 if checked
1132 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1142 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1133 end
1143 end
1134 end
1144 end
1135
1145
1136 def context_menu(url)
1146 def context_menu(url)
1137 unless @context_menu_included
1147 unless @context_menu_included
1138 content_for :header_tags do
1148 content_for :header_tags do
1139 javascript_include_tag('context_menu') +
1149 javascript_include_tag('context_menu') +
1140 stylesheet_link_tag('context_menu')
1150 stylesheet_link_tag('context_menu')
1141 end
1151 end
1142 if l(:direction) == 'rtl'
1152 if l(:direction) == 'rtl'
1143 content_for :header_tags do
1153 content_for :header_tags do
1144 stylesheet_link_tag('context_menu_rtl')
1154 stylesheet_link_tag('context_menu_rtl')
1145 end
1155 end
1146 end
1156 end
1147 @context_menu_included = true
1157 @context_menu_included = true
1148 end
1158 end
1149 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1159 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1150 end
1160 end
1151
1161
1152 def calendar_for(field_id)
1162 def calendar_for(field_id)
1153 include_calendar_headers_tags
1163 include_calendar_headers_tags
1154 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1164 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepicker(datepickerOptions); });")
1155 end
1165 end
1156
1166
1157 def include_calendar_headers_tags
1167 def include_calendar_headers_tags
1158 unless @calendar_headers_tags_included
1168 unless @calendar_headers_tags_included
1159 tags = ''.html_safe
1169 tags = ''.html_safe
1160 @calendar_headers_tags_included = true
1170 @calendar_headers_tags_included = true
1161 content_for :header_tags do
1171 content_for :header_tags do
1162 start_of_week = Setting.start_of_week
1172 start_of_week = Setting.start_of_week
1163 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1173 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1164 # Redmine uses 1..7 (monday..sunday) in settings and locales
1174 # Redmine uses 1..7 (monday..sunday) in settings and locales
1165 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1175 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1166 start_of_week = start_of_week.to_i % 7
1176 start_of_week = start_of_week.to_i % 7
1167 tags << javascript_tag(
1177 tags << javascript_tag(
1168 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1178 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1169 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1179 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1170 path_to_image('/images/calendar.png') +
1180 path_to_image('/images/calendar.png') +
1171 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1181 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1172 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1182 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1173 "beforeShow: beforeShowDatePicker};")
1183 "beforeShow: beforeShowDatePicker};")
1174 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1184 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1175 unless jquery_locale == 'en'
1185 unless jquery_locale == 'en'
1176 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1186 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1177 end
1187 end
1178 tags
1188 tags
1179 end
1189 end
1180 end
1190 end
1181 end
1191 end
1182
1192
1183 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1193 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1184 # Examples:
1194 # Examples:
1185 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1195 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1186 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1196 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1187 #
1197 #
1188 def stylesheet_link_tag(*sources)
1198 def stylesheet_link_tag(*sources)
1189 options = sources.last.is_a?(Hash) ? sources.pop : {}
1199 options = sources.last.is_a?(Hash) ? sources.pop : {}
1190 plugin = options.delete(:plugin)
1200 plugin = options.delete(:plugin)
1191 sources = sources.map do |source|
1201 sources = sources.map do |source|
1192 if plugin
1202 if plugin
1193 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1203 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1194 elsif current_theme && current_theme.stylesheets.include?(source)
1204 elsif current_theme && current_theme.stylesheets.include?(source)
1195 current_theme.stylesheet_path(source)
1205 current_theme.stylesheet_path(source)
1196 else
1206 else
1197 source
1207 source
1198 end
1208 end
1199 end
1209 end
1200 super *sources, options
1210 super *sources, options
1201 end
1211 end
1202
1212
1203 # Overrides Rails' image_tag with themes and plugins support.
1213 # Overrides Rails' image_tag with themes and plugins support.
1204 # Examples:
1214 # Examples:
1205 # image_tag('image.png') # => picks image.png from the current theme or defaults
1215 # image_tag('image.png') # => picks image.png from the current theme or defaults
1206 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1216 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1207 #
1217 #
1208 def image_tag(source, options={})
1218 def image_tag(source, options={})
1209 if plugin = options.delete(:plugin)
1219 if plugin = options.delete(:plugin)
1210 source = "/plugin_assets/#{plugin}/images/#{source}"
1220 source = "/plugin_assets/#{plugin}/images/#{source}"
1211 elsif current_theme && current_theme.images.include?(source)
1221 elsif current_theme && current_theme.images.include?(source)
1212 source = current_theme.image_path(source)
1222 source = current_theme.image_path(source)
1213 end
1223 end
1214 super source, options
1224 super source, options
1215 end
1225 end
1216
1226
1217 # Overrides Rails' javascript_include_tag with plugins support
1227 # Overrides Rails' javascript_include_tag with plugins support
1218 # Examples:
1228 # Examples:
1219 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1229 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1220 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1230 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1221 #
1231 #
1222 def javascript_include_tag(*sources)
1232 def javascript_include_tag(*sources)
1223 options = sources.last.is_a?(Hash) ? sources.pop : {}
1233 options = sources.last.is_a?(Hash) ? sources.pop : {}
1224 if plugin = options.delete(:plugin)
1234 if plugin = options.delete(:plugin)
1225 sources = sources.map do |source|
1235 sources = sources.map do |source|
1226 if plugin
1236 if plugin
1227 "/plugin_assets/#{plugin}/javascripts/#{source}"
1237 "/plugin_assets/#{plugin}/javascripts/#{source}"
1228 else
1238 else
1229 source
1239 source
1230 end
1240 end
1231 end
1241 end
1232 end
1242 end
1233 super *sources, options
1243 super *sources, options
1234 end
1244 end
1235
1245
1236 def sidebar_content?
1246 def sidebar_content?
1237 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1247 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1238 end
1248 end
1239
1249
1240 def view_layouts_base_sidebar_hook_response
1250 def view_layouts_base_sidebar_hook_response
1241 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1251 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1242 end
1252 end
1243
1253
1244 def email_delivery_enabled?
1254 def email_delivery_enabled?
1245 !!ActionMailer::Base.perform_deliveries
1255 !!ActionMailer::Base.perform_deliveries
1246 end
1256 end
1247
1257
1248 # Returns the avatar image tag for the given +user+ if avatars are enabled
1258 # Returns the avatar image tag for the given +user+ if avatars are enabled
1249 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1259 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1250 def avatar(user, options = { })
1260 def avatar(user, options = { })
1251 if Setting.gravatar_enabled?
1261 if Setting.gravatar_enabled?
1252 options.merge!(:default => Setting.gravatar_default)
1262 options.merge!(:default => Setting.gravatar_default)
1253 email = nil
1263 email = nil
1254 if user.respond_to?(:mail)
1264 if user.respond_to?(:mail)
1255 email = user.mail
1265 email = user.mail
1256 elsif user.to_s =~ %r{<(.+?)>}
1266 elsif user.to_s =~ %r{<(.+?)>}
1257 email = $1
1267 email = $1
1258 end
1268 end
1259 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1269 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1260 else
1270 else
1261 ''
1271 ''
1262 end
1272 end
1263 end
1273 end
1264
1274
1265 # Returns a link to edit user's avatar if avatars are enabled
1275 # Returns a link to edit user's avatar if avatars are enabled
1266 def avatar_edit_link(user, options={})
1276 def avatar_edit_link(user, options={})
1267 if Setting.gravatar_enabled?
1277 if Setting.gravatar_enabled?
1268 url = "https://gravatar.com"
1278 url = "https://gravatar.com"
1269 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1279 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1270 end
1280 end
1271 end
1281 end
1272
1282
1273 def sanitize_anchor_name(anchor)
1283 def sanitize_anchor_name(anchor)
1274 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1284 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1275 end
1285 end
1276
1286
1277 # Returns the javascript tags that are included in the html layout head
1287 # Returns the javascript tags that are included in the html layout head
1278 def javascript_heads
1288 def javascript_heads
1279 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1289 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1280 unless User.current.pref.warn_on_leaving_unsaved == '0'
1290 unless User.current.pref.warn_on_leaving_unsaved == '0'
1281 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1291 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1282 end
1292 end
1283 tags
1293 tags
1284 end
1294 end
1285
1295
1286 def favicon
1296 def favicon
1287 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1297 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1288 end
1298 end
1289
1299
1290 # Returns the path to the favicon
1300 # Returns the path to the favicon
1291 def favicon_path
1301 def favicon_path
1292 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1302 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1293 image_path(icon)
1303 image_path(icon)
1294 end
1304 end
1295
1305
1296 # Returns the full URL to the favicon
1306 # Returns the full URL to the favicon
1297 def favicon_url
1307 def favicon_url
1298 # TODO: use #image_url introduced in Rails4
1308 # TODO: use #image_url introduced in Rails4
1299 path = favicon_path
1309 path = favicon_path
1300 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1310 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1301 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1311 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1302 end
1312 end
1303
1313
1304 def robot_exclusion_tag
1314 def robot_exclusion_tag
1305 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1315 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1306 end
1316 end
1307
1317
1308 # Returns true if arg is expected in the API response
1318 # Returns true if arg is expected in the API response
1309 def include_in_api_response?(arg)
1319 def include_in_api_response?(arg)
1310 unless @included_in_api_response
1320 unless @included_in_api_response
1311 param = params[:include]
1321 param = params[:include]
1312 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1322 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1313 @included_in_api_response.collect!(&:strip)
1323 @included_in_api_response.collect!(&:strip)
1314 end
1324 end
1315 @included_in_api_response.include?(arg.to_s)
1325 @included_in_api_response.include?(arg.to_s)
1316 end
1326 end
1317
1327
1318 # Returns options or nil if nometa param or X-Redmine-Nometa header
1328 # Returns options or nil if nometa param or X-Redmine-Nometa header
1319 # was set in the request
1329 # was set in the request
1320 def api_meta(options)
1330 def api_meta(options)
1321 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1331 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1322 # compatibility mode for activeresource clients that raise
1332 # compatibility mode for activeresource clients that raise
1323 # an error when deserializing an array with attributes
1333 # an error when deserializing an array with attributes
1324 nil
1334 nil
1325 else
1335 else
1326 options
1336 options
1327 end
1337 end
1328 end
1338 end
1329
1339
1330 def generate_csv(&block)
1340 def generate_csv(&block)
1331 decimal_separator = l(:general_csv_decimal_separator)
1341 decimal_separator = l(:general_csv_decimal_separator)
1332 encoding = l(:general_csv_encoding)
1342 encoding = l(:general_csv_encoding)
1333 end
1343 end
1334
1344
1335 private
1345 private
1336
1346
1337 def wiki_helper
1347 def wiki_helper
1338 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1348 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1339 extend helper
1349 extend helper
1340 return self
1350 return self
1341 end
1351 end
1342
1352
1343 def link_to_content_update(text, url_params = {}, html_options = {})
1353 def link_to_content_update(text, url_params = {}, html_options = {})
1344 link_to(text, url_params, html_options)
1354 link_to(text, url_params, html_options)
1345 end
1355 end
1346 end
1356 end
@@ -1,31 +1,30
1 <table class="list">
1 <table class="list custom_fields">
2 <thead><tr>
2 <thead><tr>
3 <th><%=l(:field_name)%></th>
3 <th><%=l(:field_name)%></th>
4 <th><%=l(:field_field_format)%></th>
4 <th><%=l(:field_field_format)%></th>
5 <th><%=l(:field_is_required)%></th>
5 <th><%=l(:field_is_required)%></th>
6 <% if tab[:name] == 'IssueCustomField' %>
6 <% if tab[:name] == 'IssueCustomField' %>
7 <th><%=l(:field_is_for_all)%></th>
7 <th><%=l(:field_is_for_all)%></th>
8 <th><%=l(:label_used_by)%></th>
8 <th><%=l(:label_used_by)%></th>
9 <% end %>
9 <% end %>
10 <th><%=l(:button_sort)%></th>
11 <th></th>
10 <th></th>
12 </tr></thead>
11 </tr></thead>
13 <tbody>
12 <tbody>
14 <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
13 <% (@custom_fields_by_type[tab[:name]] || []).sort.each do |custom_field| -%>
15 <% back_url = custom_fields_path(:tab => tab[:name]) %>
14 <% back_url = custom_fields_path(:tab => tab[:name]) %>
16 <tr class="<%= cycle("odd", "even") %>">
15 <tr class="<%= cycle("odd", "even") %>">
17 <td class="name"><%= link_to custom_field.name, edit_custom_field_path(custom_field) %></td>
16 <td class="name"><%= link_to custom_field.name, edit_custom_field_path(custom_field) %></td>
18 <td><%= l(custom_field.format.label) %></td>
17 <td><%= l(custom_field.format.label) %></td>
19 <td><%= checked_image custom_field.is_required? %></td>
18 <td><%= checked_image custom_field.is_required? %></td>
20 <% if tab[:name] == 'IssueCustomField' %>
19 <% if tab[:name] == 'IssueCustomField' %>
21 <td><%= checked_image custom_field.is_for_all? %></td>
20 <td><%= checked_image custom_field.is_for_all? %></td>
22 <td><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
21 <td><%= l(:label_x_projects, :count => custom_field.projects.count) if custom_field.is_a? IssueCustomField and !custom_field.is_for_all? %></td>
23 <% end %>
22 <% end %>
24 <td class="reorder"><%= reorder_links('custom_field', {:action => 'update', :id => custom_field, :back_url => back_url}, :put) %></td>
25 <td class="buttons">
23 <td class="buttons">
24 <%= reorder_handle(custom_field, :url => custom_field_path(custom_field), :param => 'custom_field') %>
26 <%= delete_link custom_field_path(custom_field) %>
25 <%= delete_link custom_field_path(custom_field) %>
27 </td>
26 </td>
28 </tr>
27 </tr>
29 <% end; reset_cycle %>
28 <% end; reset_cycle %>
30 </tbody>
29 </tbody>
31 </table>
30 </table>
@@ -1,11 +1,15
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_custom_field_new), new_custom_field_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_custom_field_new), new_custom_field_path, :class => 'icon icon-add' %>
3 </div>
3 </div>
4
4
5 <%= title l(:label_custom_field_plural) %>
5 <%= title l(:label_custom_field_plural) %>
6
6
7 <% if @custom_fields_by_type.present? %>
7 <% if @custom_fields_by_type.present? %>
8 <%= render_custom_fields_tabs(@custom_fields_by_type.keys) %>
8 <%= render_custom_fields_tabs(@custom_fields_by_type.keys) %>
9 <% else %>
9 <% else %>
10 <p class="nodata"><%= l(:label_no_data) %></p>
10 <p class="nodata"><%= l(:label_no_data) %></p>
11 <% end %>
11 <% end %>
12
13 <%= javascript_tag do %>
14 $(function() { $("table.custom_fields tbody").positionedItems(); });
15 <% end %> No newline at end of file
@@ -1,32 +1,37
1 <h2><%=l(:label_enumerations)%></h2>
1 <h2><%=l(:label_enumerations)%></h2>
2
2
3 <% Enumeration.get_subclasses.each do |klass| %>
3 <% Enumeration.get_subclasses.each do |klass| %>
4 <h3><%= l(klass::OptionName) %></h3>
4 <h3><%= l(klass::OptionName) %></h3>
5
5
6 <% enumerations = klass.shared %>
6 <% enumerations = klass.shared %>
7 <% if enumerations.any? %>
7 <% if enumerations.any? %>
8 <table class="list"><thead>
8 <table class="list enumerations"><thead>
9 <tr>
9 <tr>
10 <th><%= l(:field_name) %></th>
10 <th><%= l(:field_name) %></th>
11 <th><%= l(:field_is_default) %></th>
11 <th><%= l(:field_is_default) %></th>
12 <th><%= l(:field_active) %></th>
12 <th><%= l(:field_active) %></th>
13 <th><%=l(:button_sort)%></th>
14 <th></th>
13 <th></th>
15 </tr></thead>
14 </tr></thead>
16 <% enumerations.each do |enumeration| %>
15 <% enumerations.each do |enumeration| %>
17 <tr class="<%= cycle('odd', 'even') %>">
16 <tr class="<%= cycle('odd', 'even') %>">
18 <td class="name"><%= link_to enumeration, edit_enumeration_path(enumeration) %></td>
17 <td class="name"><%= link_to enumeration, edit_enumeration_path(enumeration) %></td>
19 <td class="tick"><%= checked_image enumeration.is_default? %></td>
18 <td class="tick"><%= checked_image enumeration.is_default? %></td>
20 <td class="tick"><%= checked_image enumeration.active? %></td>
19 <td class="tick"><%= checked_image enumeration.active? %></td>
21 <td class="reorder"><%= reorder_links('enumeration', {:action => 'update', :id => enumeration}, :put) %></td>
20 <td class="buttons">
22 <td class="buttons"><%= delete_link enumeration_path(enumeration) %></td>
21 <%= reorder_handle(enumeration, :url => enumeration_path(enumeration), :param => 'enumeration') %>
22 <%= delete_link enumeration_path(enumeration) %>
23 </td>
23 </tr>
24 </tr>
24 <% end %>
25 <% end %>
25 </table>
26 </table>
26 <% reset_cycle %>
27 <% reset_cycle %>
27 <% end %>
28 <% end %>
28
29
29 <p><%= link_to l(:label_enumeration_new), new_enumeration_path(:type => klass.name) %></p>
30 <p><%= link_to l(:label_enumeration_new), new_enumeration_path(:type => klass.name) %></p>
30 <% end %>
31 <% end %>
31
32
32 <% html_title(l(:label_enumerations)) -%>
33 <% html_title(l(:label_enumerations)) -%>
34
35 <%= javascript_tag do %>
36 $(function() { $("table.enumerations tbody").positionedItems(); });
37 <% end %> No newline at end of file
@@ -1,35 +1,38
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_issue_status_new), new_issue_status_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_issue_status_new), new_issue_status_path, :class => 'icon icon-add' %>
3 <%= link_to(l(:label_update_issue_done_ratios), update_issue_done_ratio_issue_statuses_path, :class => 'icon icon-multiple', :method => 'post', :data => {:confirm => l(:text_are_you_sure)}) if Issue.use_status_for_done_ratio? %>
3 <%= link_to(l(:label_update_issue_done_ratios), update_issue_done_ratio_issue_statuses_path, :class => 'icon icon-multiple', :method => 'post', :data => {:confirm => l(:text_are_you_sure)}) if Issue.use_status_for_done_ratio? %>
4 </div>
4 </div>
5
5
6 <h2><%=l(:label_issue_status_plural)%></h2>
6 <h2><%=l(:label_issue_status_plural)%></h2>
7
7
8 <table class="list">
8 <table class="list issue_statuses">
9 <thead><tr>
9 <thead><tr>
10 <th><%=l(:field_status)%></th>
10 <th><%=l(:field_status)%></th>
11 <% if Issue.use_status_for_done_ratio? %>
11 <% if Issue.use_status_for_done_ratio? %>
12 <th><%=l(:field_done_ratio)%></th>
12 <th><%=l(:field_done_ratio)%></th>
13 <% end %>
13 <% end %>
14 <th><%=l(:field_is_closed)%></th>
14 <th><%=l(:field_is_closed)%></th>
15 <th><%=l(:button_sort)%></th>
16 <th></th>
15 <th></th>
17 </tr></thead>
16 </tr></thead>
18 <tbody>
17 <tbody>
19 <% for status in @issue_statuses %>
18 <% for status in @issue_statuses %>
20 <tr class="<%= cycle("odd", "even") %>">
19 <tr class="<%= cycle("odd", "even") %>">
21 <td class="name"><%= link_to status.name, edit_issue_status_path(status) %></td>
20 <td class="name"><%= link_to status.name, edit_issue_status_path(status) %></td>
22 <% if Issue.use_status_for_done_ratio? %>
21 <% if Issue.use_status_for_done_ratio? %>
23 <td><%= status.default_done_ratio %></td>
22 <td><%= status.default_done_ratio %></td>
24 <% end %>
23 <% end %>
25 <td><%= checked_image status.is_closed? %></td>
24 <td><%= checked_image status.is_closed? %></td>
26 <td class="reorder"><%= reorder_links('issue_status', {:action => 'update', :id => status, :page => params[:page]}, :put) %></td>
27 <td class="buttons">
25 <td class="buttons">
26 <%= reorder_handle(status) %>
28 <%= delete_link issue_status_path(status) %>
27 <%= delete_link issue_status_path(status) %>
29 </td>
28 </td>
30 </tr>
29 </tr>
31 <% end %>
30 <% end %>
32 </tbody>
31 </tbody>
33 </table>
32 </table>
34
33
35 <% html_title(l(:label_issue_status_plural)) -%>
34 <% html_title(l(:label_issue_status_plural)) -%>
35
36 <%= javascript_tag do %>
37 $(function() { $("table.issue_statuses tbody").positionedItems(); });
38 <% end %>
@@ -1,32 +1,31
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_role_new), new_role_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_role_new), new_role_path, :class => 'icon icon-add' %>
3 <%= link_to l(:label_permissions_report), permissions_roles_path, :class => 'icon icon-summary' %>
3 <%= link_to l(:label_permissions_report), permissions_roles_path, :class => 'icon icon-summary' %>
4 </div>
4 </div>
5
5
6 <h2><%=l(:label_role_plural)%></h2>
6 <h2><%=l(:label_role_plural)%></h2>
7
7
8 <table class="list">
8 <table class="list roles">
9 <thead><tr>
9 <thead><tr>
10 <th><%=l(:label_role)%></th>
10 <th><%=l(:label_role)%></th>
11 <th><%=l(:button_sort)%></th>
12 <th></th>
11 <th></th>
13 </tr></thead>
12 </tr></thead>
14 <tbody>
13 <tbody>
15 <% for role in @roles %>
14 <% for role in @roles %>
16 <tr class="<%= cycle("odd", "even") %>">
15 <tr class="<%= cycle("odd", "even") %> <%= role.builtin? ? "builtin" : "givable" %>">
17 <td class="name"><%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %></td>
16 <td class="name"><%= content_tag(role.builtin? ? 'em' : 'span', link_to(role.name, edit_role_path(role))) %></td>
18 <td class="reorder">
19 <% unless role.builtin? %>
20 <%= reorder_links('role', {:action => 'update', :id => role, :page => params[:page]}, :put) %>
21 <% end %>
22 </td>
23 <td class="buttons">
17 <td class="buttons">
18 <%= reorder_handle(role) unless role.builtin? %>
24 <%= link_to l(:button_copy), new_role_path(:copy => role), :class => 'icon icon-copy' %>
19 <%= link_to l(:button_copy), new_role_path(:copy => role), :class => 'icon icon-copy' %>
25 <%= delete_link role_path(role) unless role.builtin? %>
20 <%= delete_link role_path(role) unless role.builtin? %>
26 </td>
21 </td>
27 </tr>
22 </tr>
28 <% end %>
23 <% end %>
29 </tbody>
24 </tbody>
30 </table>
25 </table>
31
26
32 <% html_title(l(:label_role_plural)) -%>
27 <% html_title(l(:label_role_plural)) -%>
28
29 <%= javascript_tag do %>
30 $(function() { $("table.roles tbody").positionedItems({items: ".givable"}); });
31 <% end %> No newline at end of file
@@ -1,37 +1,38
1 <div class="contextual">
1 <div class="contextual">
2 <%= link_to l(:label_tracker_new), new_tracker_path, :class => 'icon icon-add' %>
2 <%= link_to l(:label_tracker_new), new_tracker_path, :class => 'icon icon-add' %>
3 <%= link_to l(:field_summary), fields_trackers_path, :class => 'icon icon-summary' %>
3 <%= link_to l(:field_summary), fields_trackers_path, :class => 'icon icon-summary' %>
4 </div>
4 </div>
5
5
6 <h2><%=l(:label_tracker_plural)%></h2>
6 <h2><%=l(:label_tracker_plural)%></h2>
7
7
8 <table class="list">
8 <table class="list trackers">
9 <thead><tr>
9 <thead><tr>
10 <th><%=l(:label_tracker)%></th>
10 <th><%=l(:label_tracker)%></th>
11 <th></th>
11 <th></th>
12 <th><%=l(:button_sort)%></th>
13 <th></th>
12 <th></th>
14 </tr></thead>
13 </tr></thead>
15 <tbody>
14 <tbody>
16 <% for tracker in @trackers %>
15 <% for tracker in @trackers %>
17 <tr class="<%= cycle("odd", "even") %>">
16 <tr class="<%= cycle("odd", "even") %>">
18 <td class="name"><%= link_to tracker.name, edit_tracker_path(tracker) %></td>
17 <td class="name"><%= link_to tracker.name, edit_tracker_path(tracker) %></td>
19 <td>
18 <td>
20 <% unless tracker.workflow_rules.count > 0 %>
19 <% unless tracker.workflow_rules.count > 0 %>
21 <span class="icon icon-warning">
20 <span class="icon icon-warning">
22 <%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), workflows_edit_path(:tracker_id => tracker) %>)
21 <%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), workflows_edit_path(:tracker_id => tracker) %>)
23 </span>
22 </span>
24 <% end %>
23 <% end %>
25 </td>
24 </td>
26 <td class="reorder">
27 <%= reorder_links('tracker', {:action => 'update', :id => tracker, :page => params[:page]}, :put) %>
28 </td>
29 <td class="buttons">
25 <td class="buttons">
26 <%= reorder_handle(tracker) %>
30 <%= delete_link tracker_path(tracker) %>
27 <%= delete_link tracker_path(tracker) %>
31 </td>
28 </td>
32 </tr>
29 </tr>
33 <% end %>
30 <% end %>
34 </tbody>
31 </tbody>
35 </table>
32 </table>
36
33
37 <% html_title(l(:label_tracker_plural)) -%>
34 <% html_title(l(:label_tracker_plural)) -%>
35
36 <%= javascript_tag do %>
37 $(function() { $("table.trackers tbody").positionedItems(); });
38 <% end %>
@@ -1,703 +1,742
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
2 Copyright (C) 2006-2016 Jean-Philippe Lang */
3
3
4 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
5 $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
6 }
6 }
7
7
8 function toggleCheckboxesBySelector(selector) {
8 function toggleCheckboxesBySelector(selector) {
9 var all_checked = true;
9 var all_checked = true;
10 $(selector).each(function(index) {
10 $(selector).each(function(index) {
11 if (!$(this).is(':checked')) { all_checked = false; }
11 if (!$(this).is(':checked')) { all_checked = false; }
12 });
12 });
13 $(selector).prop('checked', !all_checked);
13 $(selector).prop('checked', !all_checked);
14 }
14 }
15
15
16 function showAndScrollTo(id, focus) {
16 function showAndScrollTo(id, focus) {
17 $('#'+id).show();
17 $('#'+id).show();
18 if (focus !== null) {
18 if (focus !== null) {
19 $('#'+focus).focus();
19 $('#'+focus).focus();
20 }
20 }
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
21 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
22 }
22 }
23
23
24 function toggleRowGroup(el) {
24 function toggleRowGroup(el) {
25 var tr = $(el).parents('tr').first();
25 var tr = $(el).parents('tr').first();
26 var n = tr.next();
26 var n = tr.next();
27 tr.toggleClass('open');
27 tr.toggleClass('open');
28 while (n.length && !n.hasClass('group')) {
28 while (n.length && !n.hasClass('group')) {
29 n.toggle();
29 n.toggle();
30 n = n.next('tr');
30 n = n.next('tr');
31 }
31 }
32 }
32 }
33
33
34 function collapseAllRowGroups(el) {
34 function collapseAllRowGroups(el) {
35 var tbody = $(el).parents('tbody').first();
35 var tbody = $(el).parents('tbody').first();
36 tbody.children('tr').each(function(index) {
36 tbody.children('tr').each(function(index) {
37 if ($(this).hasClass('group')) {
37 if ($(this).hasClass('group')) {
38 $(this).removeClass('open');
38 $(this).removeClass('open');
39 } else {
39 } else {
40 $(this).hide();
40 $(this).hide();
41 }
41 }
42 });
42 });
43 }
43 }
44
44
45 function expandAllRowGroups(el) {
45 function expandAllRowGroups(el) {
46 var tbody = $(el).parents('tbody').first();
46 var tbody = $(el).parents('tbody').first();
47 tbody.children('tr').each(function(index) {
47 tbody.children('tr').each(function(index) {
48 if ($(this).hasClass('group')) {
48 if ($(this).hasClass('group')) {
49 $(this).addClass('open');
49 $(this).addClass('open');
50 } else {
50 } else {
51 $(this).show();
51 $(this).show();
52 }
52 }
53 });
53 });
54 }
54 }
55
55
56 function toggleAllRowGroups(el) {
56 function toggleAllRowGroups(el) {
57 var tr = $(el).parents('tr').first();
57 var tr = $(el).parents('tr').first();
58 if (tr.hasClass('open')) {
58 if (tr.hasClass('open')) {
59 collapseAllRowGroups(el);
59 collapseAllRowGroups(el);
60 } else {
60 } else {
61 expandAllRowGroups(el);
61 expandAllRowGroups(el);
62 }
62 }
63 }
63 }
64
64
65 function toggleFieldset(el) {
65 function toggleFieldset(el) {
66 var fieldset = $(el).parents('fieldset').first();
66 var fieldset = $(el).parents('fieldset').first();
67 fieldset.toggleClass('collapsed');
67 fieldset.toggleClass('collapsed');
68 fieldset.children('div').toggle();
68 fieldset.children('div').toggle();
69 }
69 }
70
70
71 function hideFieldset(el) {
71 function hideFieldset(el) {
72 var fieldset = $(el).parents('fieldset').first();
72 var fieldset = $(el).parents('fieldset').first();
73 fieldset.toggleClass('collapsed');
73 fieldset.toggleClass('collapsed');
74 fieldset.children('div').hide();
74 fieldset.children('div').hide();
75 }
75 }
76
76
77 // columns selection
77 // columns selection
78 function moveOptions(theSelFrom, theSelTo) {
78 function moveOptions(theSelFrom, theSelTo) {
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
79 $(theSelFrom).find('option:selected').detach().prop("selected", false).appendTo($(theSelTo));
80 }
80 }
81
81
82 function moveOptionUp(theSel) {
82 function moveOptionUp(theSel) {
83 $(theSel).find('option:selected').each(function(){
83 $(theSel).find('option:selected').each(function(){
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
84 $(this).prev(':not(:selected)').detach().insertAfter($(this));
85 });
85 });
86 }
86 }
87
87
88 function moveOptionTop(theSel) {
88 function moveOptionTop(theSel) {
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
89 $(theSel).find('option:selected').detach().prependTo($(theSel));
90 }
90 }
91
91
92 function moveOptionDown(theSel) {
92 function moveOptionDown(theSel) {
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
93 $($(theSel).find('option:selected').get().reverse()).each(function(){
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
94 $(this).next(':not(:selected)').detach().insertBefore($(this));
95 });
95 });
96 }
96 }
97
97
98 function moveOptionBottom(theSel) {
98 function moveOptionBottom(theSel) {
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
99 $(theSel).find('option:selected').detach().appendTo($(theSel));
100 }
100 }
101
101
102 function initFilters() {
102 function initFilters() {
103 $('#add_filter_select').change(function() {
103 $('#add_filter_select').change(function() {
104 addFilter($(this).val(), '', []);
104 addFilter($(this).val(), '', []);
105 });
105 });
106 $('#filters-table td.field input[type=checkbox]').each(function() {
106 $('#filters-table td.field input[type=checkbox]').each(function() {
107 toggleFilter($(this).val());
107 toggleFilter($(this).val());
108 });
108 });
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
109 $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
110 toggleFilter($(this).val());
110 toggleFilter($(this).val());
111 });
111 });
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
112 $('#filters-table').on('click', '.toggle-multiselect', function() {
113 toggleMultiSelect($(this).siblings('select'));
113 toggleMultiSelect($(this).siblings('select'));
114 });
114 });
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
115 $('#filters-table').on('keypress', 'input[type=text]', function(e) {
116 if (e.keyCode == 13) $(this).closest('form').submit();
116 if (e.keyCode == 13) $(this).closest('form').submit();
117 });
117 });
118 }
118 }
119
119
120 function addFilter(field, operator, values) {
120 function addFilter(field, operator, values) {
121 var fieldId = field.replace('.', '_');
121 var fieldId = field.replace('.', '_');
122 var tr = $('#tr_'+fieldId);
122 var tr = $('#tr_'+fieldId);
123 if (tr.length > 0) {
123 if (tr.length > 0) {
124 tr.show();
124 tr.show();
125 } else {
125 } else {
126 buildFilterRow(field, operator, values);
126 buildFilterRow(field, operator, values);
127 }
127 }
128 $('#cb_'+fieldId).prop('checked', true);
128 $('#cb_'+fieldId).prop('checked', true);
129 toggleFilter(field);
129 toggleFilter(field);
130 $('#add_filter_select').val('').find('option').each(function() {
130 $('#add_filter_select').val('').find('option').each(function() {
131 if ($(this).attr('value') == field) {
131 if ($(this).attr('value') == field) {
132 $(this).attr('disabled', true);
132 $(this).attr('disabled', true);
133 }
133 }
134 });
134 });
135 }
135 }
136
136
137 function buildFilterRow(field, operator, values) {
137 function buildFilterRow(field, operator, values) {
138 var fieldId = field.replace('.', '_');
138 var fieldId = field.replace('.', '_');
139 var filterTable = $("#filters-table");
139 var filterTable = $("#filters-table");
140 var filterOptions = availableFilters[field];
140 var filterOptions = availableFilters[field];
141 if (!filterOptions) return;
141 if (!filterOptions) return;
142 var operators = operatorByType[filterOptions['type']];
142 var operators = operatorByType[filterOptions['type']];
143 var filterValues = filterOptions['values'];
143 var filterValues = filterOptions['values'];
144 var i, select;
144 var i, select;
145
145
146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
146 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
147 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
148 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
149 '<td class="values"></td>'
149 '<td class="values"></td>'
150 );
150 );
151 filterTable.append(tr);
151 filterTable.append(tr);
152
152
153 select = tr.find('td.operator select');
153 select = tr.find('td.operator select');
154 for (i = 0; i < operators.length; i++) {
154 for (i = 0; i < operators.length; i++) {
155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
155 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
156 if (operators[i] == operator) { option.attr('selected', true); }
156 if (operators[i] == operator) { option.attr('selected', true); }
157 select.append(option);
157 select.append(option);
158 }
158 }
159 select.change(function(){ toggleOperator(field); });
159 select.change(function(){ toggleOperator(field); });
160
160
161 switch (filterOptions['type']) {
161 switch (filterOptions['type']) {
162 case "list":
162 case "list":
163 case "list_optional":
163 case "list_optional":
164 case "list_status":
164 case "list_status":
165 case "list_subprojects":
165 case "list_subprojects":
166 tr.find('td.values').append(
166 tr.find('td.values').append(
167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
167 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
168 ' <span class="toggle-multiselect">&nbsp;</span></span>'
169 );
169 );
170 select = tr.find('td.values select');
170 select = tr.find('td.values select');
171 if (values.length > 1) { select.attr('multiple', true); }
171 if (values.length > 1) { select.attr('multiple', true); }
172 for (i = 0; i < filterValues.length; i++) {
172 for (i = 0; i < filterValues.length; i++) {
173 var filterValue = filterValues[i];
173 var filterValue = filterValues[i];
174 var option = $('<option>');
174 var option = $('<option>');
175 if ($.isArray(filterValue)) {
175 if ($.isArray(filterValue)) {
176 option.val(filterValue[1]).text(filterValue[0]);
176 option.val(filterValue[1]).text(filterValue[0]);
177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
177 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
178 } else {
178 } else {
179 option.val(filterValue).text(filterValue);
179 option.val(filterValue).text(filterValue);
180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
180 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
181 }
181 }
182 select.append(option);
182 select.append(option);
183 }
183 }
184 break;
184 break;
185 case "date":
185 case "date":
186 case "date_past":
186 case "date_past":
187 tr.find('td.values').append(
187 tr.find('td.values').append(
188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
188 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
189 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
190 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
191 );
191 );
192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
192 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
193 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
194 $('#values_'+fieldId).val(values[0]);
194 $('#values_'+fieldId).val(values[0]);
195 break;
195 break;
196 case "string":
196 case "string":
197 case "text":
197 case "text":
198 tr.find('td.values').append(
198 tr.find('td.values').append(
199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
199 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
200 );
200 );
201 $('#values_'+fieldId).val(values[0]);
201 $('#values_'+fieldId).val(values[0]);
202 break;
202 break;
203 case "relation":
203 case "relation":
204 tr.find('td.values').append(
204 tr.find('td.values').append(
205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
205 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
206 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
207 );
207 );
208 $('#values_'+fieldId).val(values[0]);
208 $('#values_'+fieldId).val(values[0]);
209 select = tr.find('td.values select');
209 select = tr.find('td.values select');
210 for (i = 0; i < allProjects.length; i++) {
210 for (i = 0; i < allProjects.length; i++) {
211 var filterValue = allProjects[i];
211 var filterValue = allProjects[i];
212 var option = $('<option>');
212 var option = $('<option>');
213 option.val(filterValue[1]).text(filterValue[0]);
213 option.val(filterValue[1]).text(filterValue[0]);
214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
214 if (values[0] == filterValue[1]) { option.attr('selected', true); }
215 select.append(option);
215 select.append(option);
216 }
216 }
217 break;
217 break;
218 case "integer":
218 case "integer":
219 case "float":
219 case "float":
220 case "tree":
220 case "tree":
221 tr.find('td.values').append(
221 tr.find('td.values').append(
222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
222 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
223 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
224 );
224 );
225 $('#values_'+fieldId+'_1').val(values[0]);
225 $('#values_'+fieldId+'_1').val(values[0]);
226 $('#values_'+fieldId+'_2').val(values[1]);
226 $('#values_'+fieldId+'_2').val(values[1]);
227 break;
227 break;
228 }
228 }
229 }
229 }
230
230
231 function toggleFilter(field) {
231 function toggleFilter(field) {
232 var fieldId = field.replace('.', '_');
232 var fieldId = field.replace('.', '_');
233 if ($('#cb_' + fieldId).is(':checked')) {
233 if ($('#cb_' + fieldId).is(':checked')) {
234 $("#operators_" + fieldId).show().removeAttr('disabled');
234 $("#operators_" + fieldId).show().removeAttr('disabled');
235 toggleOperator(field);
235 toggleOperator(field);
236 } else {
236 } else {
237 $("#operators_" + fieldId).hide().attr('disabled', true);
237 $("#operators_" + fieldId).hide().attr('disabled', true);
238 enableValues(field, []);
238 enableValues(field, []);
239 }
239 }
240 }
240 }
241
241
242 function enableValues(field, indexes) {
242 function enableValues(field, indexes) {
243 var fieldId = field.replace('.', '_');
243 var fieldId = field.replace('.', '_');
244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
244 $('#tr_'+fieldId+' td.values .value').each(function(index) {
245 if ($.inArray(index, indexes) >= 0) {
245 if ($.inArray(index, indexes) >= 0) {
246 $(this).removeAttr('disabled');
246 $(this).removeAttr('disabled');
247 $(this).parents('span').first().show();
247 $(this).parents('span').first().show();
248 } else {
248 } else {
249 $(this).val('');
249 $(this).val('');
250 $(this).attr('disabled', true);
250 $(this).attr('disabled', true);
251 $(this).parents('span').first().hide();
251 $(this).parents('span').first().hide();
252 }
252 }
253
253
254 if ($(this).hasClass('group')) {
254 if ($(this).hasClass('group')) {
255 $(this).addClass('open');
255 $(this).addClass('open');
256 } else {
256 } else {
257 $(this).show();
257 $(this).show();
258 }
258 }
259 });
259 });
260 }
260 }
261
261
262 function toggleOperator(field) {
262 function toggleOperator(field) {
263 var fieldId = field.replace('.', '_');
263 var fieldId = field.replace('.', '_');
264 var operator = $("#operators_" + fieldId);
264 var operator = $("#operators_" + fieldId);
265 switch (operator.val()) {
265 switch (operator.val()) {
266 case "!*":
266 case "!*":
267 case "*":
267 case "*":
268 case "t":
268 case "t":
269 case "ld":
269 case "ld":
270 case "w":
270 case "w":
271 case "lw":
271 case "lw":
272 case "l2w":
272 case "l2w":
273 case "m":
273 case "m":
274 case "lm":
274 case "lm":
275 case "y":
275 case "y":
276 case "o":
276 case "o":
277 case "c":
277 case "c":
278 case "*o":
278 case "*o":
279 case "!o":
279 case "!o":
280 enableValues(field, []);
280 enableValues(field, []);
281 break;
281 break;
282 case "><":
282 case "><":
283 enableValues(field, [0,1]);
283 enableValues(field, [0,1]);
284 break;
284 break;
285 case "<t+":
285 case "<t+":
286 case ">t+":
286 case ">t+":
287 case "><t+":
287 case "><t+":
288 case "t+":
288 case "t+":
289 case ">t-":
289 case ">t-":
290 case "<t-":
290 case "<t-":
291 case "><t-":
291 case "><t-":
292 case "t-":
292 case "t-":
293 enableValues(field, [2]);
293 enableValues(field, [2]);
294 break;
294 break;
295 case "=p":
295 case "=p":
296 case "=!p":
296 case "=!p":
297 case "!p":
297 case "!p":
298 enableValues(field, [1]);
298 enableValues(field, [1]);
299 break;
299 break;
300 default:
300 default:
301 enableValues(field, [0]);
301 enableValues(field, [0]);
302 break;
302 break;
303 }
303 }
304 }
304 }
305
305
306 function toggleMultiSelect(el) {
306 function toggleMultiSelect(el) {
307 if (el.attr('multiple')) {
307 if (el.attr('multiple')) {
308 el.removeAttr('multiple');
308 el.removeAttr('multiple');
309 el.attr('size', 1);
309 el.attr('size', 1);
310 } else {
310 } else {
311 el.attr('multiple', true);
311 el.attr('multiple', true);
312 if (el.children().length > 10)
312 if (el.children().length > 10)
313 el.attr('size', 10);
313 el.attr('size', 10);
314 else
314 else
315 el.attr('size', 4);
315 el.attr('size', 4);
316 }
316 }
317 }
317 }
318
318
319 function showTab(name, url) {
319 function showTab(name, url) {
320 $('#tab-content-' + name).parent().find('.tab-content').hide();
320 $('#tab-content-' + name).parent().find('.tab-content').hide();
321 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
321 $('#tab-content-' + name).parent().find('div.tabs a').removeClass('selected');
322 $('#tab-content-' + name).show();
322 $('#tab-content-' + name).show();
323 $('#tab-' + name).addClass('selected');
323 $('#tab-' + name).addClass('selected');
324 //replaces current URL with the "href" attribute of the current link
324 //replaces current URL with the "href" attribute of the current link
325 //(only triggered if supported by browser)
325 //(only triggered if supported by browser)
326 if ("replaceState" in window.history) {
326 if ("replaceState" in window.history) {
327 window.history.replaceState(null, document.title, url);
327 window.history.replaceState(null, document.title, url);
328 }
328 }
329 return false;
329 return false;
330 }
330 }
331
331
332 function moveTabRight(el) {
332 function moveTabRight(el) {
333 var lis = $(el).parents('div.tabs').first().find('ul').children();
333 var lis = $(el).parents('div.tabs').first().find('ul').children();
334 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
334 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
335 var tabsWidth = 0;
335 var tabsWidth = 0;
336 var i = 0;
336 var i = 0;
337 lis.each(function() {
337 lis.each(function() {
338 if ($(this).is(':visible')) {
338 if ($(this).is(':visible')) {
339 tabsWidth += $(this).outerWidth(true);
339 tabsWidth += $(this).outerWidth(true);
340 }
340 }
341 });
341 });
342 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
342 if (tabsWidth < $(el).parents('div.tabs').first().width() - bw) { return; }
343 $(el).siblings('.tab-left').removeClass('disabled');
343 $(el).siblings('.tab-left').removeClass('disabled');
344 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
344 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
345 var w = lis.eq(i).width();
345 var w = lis.eq(i).width();
346 lis.eq(i).hide();
346 lis.eq(i).hide();
347 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
347 if (tabsWidth - w < $(el).parents('div.tabs').first().width() - bw) {
348 $(el).addClass('disabled');
348 $(el).addClass('disabled');
349 }
349 }
350 }
350 }
351
351
352 function moveTabLeft(el) {
352 function moveTabLeft(el) {
353 var lis = $(el).parents('div.tabs').first().find('ul').children();
353 var lis = $(el).parents('div.tabs').first().find('ul').children();
354 var i = 0;
354 var i = 0;
355 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
355 while (i < lis.length && !lis.eq(i).is(':visible')) { i++; }
356 if (i > 0) {
356 if (i > 0) {
357 lis.eq(i-1).show();
357 lis.eq(i-1).show();
358 $(el).siblings('.tab-right').removeClass('disabled');
358 $(el).siblings('.tab-right').removeClass('disabled');
359 }
359 }
360 if (i <= 1) {
360 if (i <= 1) {
361 $(el).addClass('disabled');
361 $(el).addClass('disabled');
362 }
362 }
363 }
363 }
364
364
365 function displayTabsButtons() {
365 function displayTabsButtons() {
366 var lis;
366 var lis;
367 var tabsWidth;
367 var tabsWidth;
368 var el;
368 var el;
369 var numHidden;
369 var numHidden;
370 $('div.tabs').each(function() {
370 $('div.tabs').each(function() {
371 el = $(this);
371 el = $(this);
372 lis = el.find('ul').children();
372 lis = el.find('ul').children();
373 tabsWidth = 0;
373 tabsWidth = 0;
374 numHidden = 0;
374 numHidden = 0;
375 lis.each(function(){
375 lis.each(function(){
376 if ($(this).is(':visible')) {
376 if ($(this).is(':visible')) {
377 tabsWidth += $(this).outerWidth(true);
377 tabsWidth += $(this).outerWidth(true);
378 } else {
378 } else {
379 numHidden++;
379 numHidden++;
380 }
380 }
381 });
381 });
382 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
382 var bw = $(el).parents('div.tabs-buttons').outerWidth(true);
383 if ((tabsWidth < el.width() - bw) && (lis.first().is(':visible'))) {
383 if ((tabsWidth < el.width() - bw) && (lis.first().is(':visible'))) {
384 el.find('div.tabs-buttons').hide();
384 el.find('div.tabs-buttons').hide();
385 } else {
385 } else {
386 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
386 el.find('div.tabs-buttons').show().children('button.tab-left').toggleClass('disabled', numHidden == 0);
387 }
387 }
388 });
388 });
389 }
389 }
390
390
391 function setPredecessorFieldsVisibility() {
391 function setPredecessorFieldsVisibility() {
392 var relationType = $('#relation_relation_type');
392 var relationType = $('#relation_relation_type');
393 if (relationType.val() == "precedes" || relationType.val() == "follows") {
393 if (relationType.val() == "precedes" || relationType.val() == "follows") {
394 $('#predecessor_fields').show();
394 $('#predecessor_fields').show();
395 } else {
395 } else {
396 $('#predecessor_fields').hide();
396 $('#predecessor_fields').hide();
397 }
397 }
398 }
398 }
399
399
400 function showModal(id, width, title) {
400 function showModal(id, width, title) {
401 var el = $('#'+id).first();
401 var el = $('#'+id).first();
402 if (el.length === 0 || el.is(':visible')) {return;}
402 if (el.length === 0 || el.is(':visible')) {return;}
403 if (!title) title = el.find('h3.title').text();
403 if (!title) title = el.find('h3.title').text();
404 // moves existing modals behind the transparent background
404 // moves existing modals behind the transparent background
405 $(".modal").zIndex(99);
405 $(".modal").zIndex(99);
406 el.dialog({
406 el.dialog({
407 width: width,
407 width: width,
408 modal: true,
408 modal: true,
409 resizable: false,
409 resizable: false,
410 dialogClass: 'modal',
410 dialogClass: 'modal',
411 title: title
411 title: title
412 }).on('dialogclose', function(){
412 }).on('dialogclose', function(){
413 $(".modal").zIndex(101);
413 $(".modal").zIndex(101);
414 });
414 });
415 el.find("input[type=text], input[type=submit]").first().focus();
415 el.find("input[type=text], input[type=submit]").first().focus();
416 }
416 }
417
417
418 function hideModal(el) {
418 function hideModal(el) {
419 var modal;
419 var modal;
420 if (el) {
420 if (el) {
421 modal = $(el).parents('.ui-dialog-content');
421 modal = $(el).parents('.ui-dialog-content');
422 } else {
422 } else {
423 modal = $('#ajax-modal');
423 modal = $('#ajax-modal');
424 }
424 }
425 modal.dialog("close");
425 modal.dialog("close");
426 }
426 }
427
427
428 function submitPreview(url, form, target) {
428 function submitPreview(url, form, target) {
429 $.ajax({
429 $.ajax({
430 url: url,
430 url: url,
431 type: 'post',
431 type: 'post',
432 data: $('#'+form).serialize(),
432 data: $('#'+form).serialize(),
433 success: function(data){
433 success: function(data){
434 $('#'+target).html(data);
434 $('#'+target).html(data);
435 }
435 }
436 });
436 });
437 }
437 }
438
438
439 function collapseScmEntry(id) {
439 function collapseScmEntry(id) {
440 $('.'+id).each(function() {
440 $('.'+id).each(function() {
441 if ($(this).hasClass('open')) {
441 if ($(this).hasClass('open')) {
442 collapseScmEntry($(this).attr('id'));
442 collapseScmEntry($(this).attr('id'));
443 }
443 }
444 $(this).hide();
444 $(this).hide();
445 });
445 });
446 $('#'+id).removeClass('open');
446 $('#'+id).removeClass('open');
447 }
447 }
448
448
449 function expandScmEntry(id) {
449 function expandScmEntry(id) {
450 $('.'+id).each(function() {
450 $('.'+id).each(function() {
451 $(this).show();
451 $(this).show();
452 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
452 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
453 expandScmEntry($(this).attr('id'));
453 expandScmEntry($(this).attr('id'));
454 }
454 }
455 });
455 });
456 $('#'+id).addClass('open');
456 $('#'+id).addClass('open');
457 }
457 }
458
458
459 function scmEntryClick(id, url) {
459 function scmEntryClick(id, url) {
460 var el = $('#'+id);
460 var el = $('#'+id);
461 if (el.hasClass('open')) {
461 if (el.hasClass('open')) {
462 collapseScmEntry(id);
462 collapseScmEntry(id);
463 el.addClass('collapsed');
463 el.addClass('collapsed');
464 return false;
464 return false;
465 } else if (el.hasClass('loaded')) {
465 } else if (el.hasClass('loaded')) {
466 expandScmEntry(id);
466 expandScmEntry(id);
467 el.removeClass('collapsed');
467 el.removeClass('collapsed');
468 return false;
468 return false;
469 }
469 }
470 if (el.hasClass('loading')) {
470 if (el.hasClass('loading')) {
471 return false;
471 return false;
472 }
472 }
473 el.addClass('loading');
473 el.addClass('loading');
474 $.ajax({
474 $.ajax({
475 url: url,
475 url: url,
476 success: function(data) {
476 success: function(data) {
477 el.after(data);
477 el.after(data);
478 el.addClass('open').addClass('loaded').removeClass('loading');
478 el.addClass('open').addClass('loaded').removeClass('loading');
479 }
479 }
480 });
480 });
481 return true;
481 return true;
482 }
482 }
483
483
484 function randomKey(size) {
484 function randomKey(size) {
485 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
485 var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
486 var key = '';
486 var key = '';
487 for (var i = 0; i < size; i++) {
487 for (var i = 0; i < size; i++) {
488 key += chars.charAt(Math.floor(Math.random() * chars.length));
488 key += chars.charAt(Math.floor(Math.random() * chars.length));
489 }
489 }
490 return key;
490 return key;
491 }
491 }
492
492
493 function updateIssueFrom(url, el) {
493 function updateIssueFrom(url, el) {
494 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
494 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
495 $(this).data('valuebeforeupdate', $(this).val());
495 $(this).data('valuebeforeupdate', $(this).val());
496 });
496 });
497 if (el) {
497 if (el) {
498 $("#form_update_triggered_by").val($(el).attr('id'));
498 $("#form_update_triggered_by").val($(el).attr('id'));
499 }
499 }
500 return $.ajax({
500 return $.ajax({
501 url: url,
501 url: url,
502 type: 'post',
502 type: 'post',
503 data: $('#issue-form').serialize()
503 data: $('#issue-form').serialize()
504 });
504 });
505 }
505 }
506
506
507 function replaceIssueFormWith(html){
507 function replaceIssueFormWith(html){
508 var replacement = $(html);
508 var replacement = $(html);
509 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
509 $('#all_attributes input, #all_attributes textarea, #all_attributes select').each(function(){
510 var object_id = $(this).attr('id');
510 var object_id = $(this).attr('id');
511 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
511 if (object_id && $(this).data('valuebeforeupdate')!=$(this).val()) {
512 replacement.find('#'+object_id).val($(this).val());
512 replacement.find('#'+object_id).val($(this).val());
513 }
513 }
514 });
514 });
515 $('#all_attributes').empty();
515 $('#all_attributes').empty();
516 $('#all_attributes').prepend(replacement);
516 $('#all_attributes').prepend(replacement);
517 }
517 }
518
518
519 function updateBulkEditFrom(url) {
519 function updateBulkEditFrom(url) {
520 $.ajax({
520 $.ajax({
521 url: url,
521 url: url,
522 type: 'post',
522 type: 'post',
523 data: $('#bulk_edit_form').serialize()
523 data: $('#bulk_edit_form').serialize()
524 });
524 });
525 }
525 }
526
526
527 function observeAutocompleteField(fieldId, url, options) {
527 function observeAutocompleteField(fieldId, url, options) {
528 $(document).ready(function() {
528 $(document).ready(function() {
529 $('#'+fieldId).autocomplete($.extend({
529 $('#'+fieldId).autocomplete($.extend({
530 source: url,
530 source: url,
531 minLength: 2,
531 minLength: 2,
532 position: {collision: "flipfit"},
532 position: {collision: "flipfit"},
533 search: function(){$('#'+fieldId).addClass('ajax-loading');},
533 search: function(){$('#'+fieldId).addClass('ajax-loading');},
534 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
534 response: function(){$('#'+fieldId).removeClass('ajax-loading');}
535 }, options));
535 }, options));
536 $('#'+fieldId).addClass('autocomplete');
536 $('#'+fieldId).addClass('autocomplete');
537 });
537 });
538 }
538 }
539
539
540 function observeSearchfield(fieldId, targetId, url) {
540 function observeSearchfield(fieldId, targetId, url) {
541 $('#'+fieldId).each(function() {
541 $('#'+fieldId).each(function() {
542 var $this = $(this);
542 var $this = $(this);
543 $this.addClass('autocomplete');
543 $this.addClass('autocomplete');
544 $this.attr('data-value-was', $this.val());
544 $this.attr('data-value-was', $this.val());
545 var check = function() {
545 var check = function() {
546 var val = $this.val();
546 var val = $this.val();
547 if ($this.attr('data-value-was') != val){
547 if ($this.attr('data-value-was') != val){
548 $this.attr('data-value-was', val);
548 $this.attr('data-value-was', val);
549 $.ajax({
549 $.ajax({
550 url: url,
550 url: url,
551 type: 'get',
551 type: 'get',
552 data: {q: $this.val()},
552 data: {q: $this.val()},
553 success: function(data){ if(targetId) $('#'+targetId).html(data); },
553 success: function(data){ if(targetId) $('#'+targetId).html(data); },
554 beforeSend: function(){ $this.addClass('ajax-loading'); },
554 beforeSend: function(){ $this.addClass('ajax-loading'); },
555 complete: function(){ $this.removeClass('ajax-loading'); }
555 complete: function(){ $this.removeClass('ajax-loading'); }
556 });
556 });
557 }
557 }
558 };
558 };
559 var reset = function() {
559 var reset = function() {
560 if (timer) {
560 if (timer) {
561 clearInterval(timer);
561 clearInterval(timer);
562 timer = setInterval(check, 300);
562 timer = setInterval(check, 300);
563 }
563 }
564 };
564 };
565 var timer = setInterval(check, 300);
565 var timer = setInterval(check, 300);
566 $this.bind('keyup click mousemove', reset);
566 $this.bind('keyup click mousemove', reset);
567 });
567 });
568 }
568 }
569
569
570 function beforeShowDatePicker(input, inst) {
570 function beforeShowDatePicker(input, inst) {
571 var default_date = null;
571 var default_date = null;
572 switch ($(input).attr("id")) {
572 switch ($(input).attr("id")) {
573 case "issue_start_date" :
573 case "issue_start_date" :
574 if ($("#issue_due_date").size() > 0) {
574 if ($("#issue_due_date").size() > 0) {
575 default_date = $("#issue_due_date").val();
575 default_date = $("#issue_due_date").val();
576 }
576 }
577 break;
577 break;
578 case "issue_due_date" :
578 case "issue_due_date" :
579 if ($("#issue_start_date").size() > 0) {
579 if ($("#issue_start_date").size() > 0) {
580 var start_date = $("#issue_start_date").val();
580 var start_date = $("#issue_start_date").val();
581 if (start_date != "") {
581 if (start_date != "") {
582 start_date = new Date(Date.parse(start_date));
582 start_date = new Date(Date.parse(start_date));
583 if (start_date > new Date()) {
583 if (start_date > new Date()) {
584 default_date = $("#issue_start_date").val();
584 default_date = $("#issue_start_date").val();
585 }
585 }
586 }
586 }
587 }
587 }
588 break;
588 break;
589 }
589 }
590 $(input).datepicker("option", "defaultDate", default_date);
590 $(input).datepicker("option", "defaultDate", default_date);
591 }
591 }
592
592
593 (function($){
594 $.fn.positionedItems = function(sortableOptions, options){
595 var settings = $.extend({
596 firstPosition: 1
597 }, options );
598
599 return this.sortable($.extend({
600 handle: ".sort-handle",
601 helper: function(event, ui){
602 ui.children().each(function(){
603 $(this).width($(this).width());
604 });
605 return ui;
606 },
607 update: function(event, ui) {
608 var sortable = $(this);
609 var url = ui.item.find(".sort-handle").data("reorder-url");
610 var param = ui.item.find(".sort-handle").data("reorder-param");
611 var data = {};
612 data[param] = {position: ui.item.index() + settings['firstPosition']};
613 $.ajax({
614 url: url,
615 type: 'put',
616 dataType: 'script',
617 data: data,
618 success: function(data){
619 sortable.children(":even").removeClass("even").addClass("odd");
620 sortable.children(":odd").removeClass("odd").addClass("even");
621 },
622 error: function(jqXHR, textStatus, errorThrown){
623 alert(jqXHR.status);
624 sortable.sortable("cancel");
625 }
626 });
627 },
628 }, sortableOptions));
629 }
630 }( jQuery ));
631
593 function initMyPageSortable(list, url) {
632 function initMyPageSortable(list, url) {
594 $('#list-'+list).sortable({
633 $('#list-'+list).sortable({
595 connectWith: '.block-receiver',
634 connectWith: '.block-receiver',
596 tolerance: 'pointer',
635 tolerance: 'pointer',
597 update: function(){
636 update: function(){
598 $.ajax({
637 $.ajax({
599 url: url,
638 url: url,
600 type: 'post',
639 type: 'post',
601 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
640 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
602 });
641 });
603 }
642 }
604 });
643 });
605 $("#list-top, #list-left, #list-right").disableSelection();
644 $("#list-top, #list-left, #list-right").disableSelection();
606 }
645 }
607
646
608 var warnLeavingUnsavedMessage;
647 var warnLeavingUnsavedMessage;
609 function warnLeavingUnsaved(message) {
648 function warnLeavingUnsaved(message) {
610 warnLeavingUnsavedMessage = message;
649 warnLeavingUnsavedMessage = message;
611 $(document).on('submit', 'form', function(){
650 $(document).on('submit', 'form', function(){
612 $('textarea').removeData('changed');
651 $('textarea').removeData('changed');
613 });
652 });
614 $(document).on('change', 'textarea', function(){
653 $(document).on('change', 'textarea', function(){
615 $(this).data('changed', 'changed');
654 $(this).data('changed', 'changed');
616 });
655 });
617 window.onbeforeunload = function(){
656 window.onbeforeunload = function(){
618 var warn = false;
657 var warn = false;
619 $('textarea').blur().each(function(){
658 $('textarea').blur().each(function(){
620 if ($(this).data('changed')) {
659 if ($(this).data('changed')) {
621 warn = true;
660 warn = true;
622 }
661 }
623 });
662 });
624 if (warn) {return warnLeavingUnsavedMessage;}
663 if (warn) {return warnLeavingUnsavedMessage;}
625 };
664 };
626 }
665 }
627
666
628 function setupAjaxIndicator() {
667 function setupAjaxIndicator() {
629 $(document).bind('ajaxSend', function(event, xhr, settings) {
668 $(document).bind('ajaxSend', function(event, xhr, settings) {
630 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
669 if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
631 $('#ajax-indicator').show();
670 $('#ajax-indicator').show();
632 }
671 }
633 });
672 });
634 $(document).bind('ajaxStop', function() {
673 $(document).bind('ajaxStop', function() {
635 $('#ajax-indicator').hide();
674 $('#ajax-indicator').hide();
636 });
675 });
637 }
676 }
638
677
639 function setupTabs() {
678 function setupTabs() {
640 if($('.tabs').length > 0) {
679 if($('.tabs').length > 0) {
641 displayTabsButtons();
680 displayTabsButtons();
642 $(window).resize(displayTabsButtons);
681 $(window).resize(displayTabsButtons);
643 }
682 }
644 }
683 }
645
684
646 function hideOnLoad() {
685 function hideOnLoad() {
647 $('.hol').hide();
686 $('.hol').hide();
648 }
687 }
649
688
650 function addFormObserversForDoubleSubmit() {
689 function addFormObserversForDoubleSubmit() {
651 $('form[method=post]').each(function() {
690 $('form[method=post]').each(function() {
652 if (!$(this).hasClass('multiple-submit')) {
691 if (!$(this).hasClass('multiple-submit')) {
653 $(this).submit(function(form_submission) {
692 $(this).submit(function(form_submission) {
654 if ($(form_submission.target).attr('data-submitted')) {
693 if ($(form_submission.target).attr('data-submitted')) {
655 form_submission.preventDefault();
694 form_submission.preventDefault();
656 } else {
695 } else {
657 $(form_submission.target).attr('data-submitted', true);
696 $(form_submission.target).attr('data-submitted', true);
658 }
697 }
659 });
698 });
660 }
699 }
661 });
700 });
662 }
701 }
663
702
664 function defaultFocus(){
703 function defaultFocus(){
665 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
704 if (($('#content :focus').length == 0) && (window.location.hash == '')) {
666 $('#content input[type=text], #content textarea').first().focus();
705 $('#content input[type=text], #content textarea').first().focus();
667 }
706 }
668 }
707 }
669
708
670 function blockEventPropagation(event) {
709 function blockEventPropagation(event) {
671 event.stopPropagation();
710 event.stopPropagation();
672 event.preventDefault();
711 event.preventDefault();
673 }
712 }
674
713
675 function toggleDisabledOnChange() {
714 function toggleDisabledOnChange() {
676 var checked = $(this).is(':checked');
715 var checked = $(this).is(':checked');
677 $($(this).data('disables')).attr('disabled', checked);
716 $($(this).data('disables')).attr('disabled', checked);
678 $($(this).data('enables')).attr('disabled', !checked);
717 $($(this).data('enables')).attr('disabled', !checked);
679 }
718 }
680 function toggleDisabledInit() {
719 function toggleDisabledInit() {
681 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
720 $('input[data-disables], input[data-enables]').each(toggleDisabledOnChange);
682 }
721 }
683 $(document).ready(function(){
722 $(document).ready(function(){
684 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
723 $('#content').on('change', 'input[data-disables], input[data-enables]', toggleDisabledOnChange);
685 toggleDisabledInit();
724 toggleDisabledInit();
686 });
725 });
687
726
688 function keepAnchorOnSignIn(form){
727 function keepAnchorOnSignIn(form){
689 var hash = decodeURIComponent(self.document.location.hash);
728 var hash = decodeURIComponent(self.document.location.hash);
690 if (hash) {
729 if (hash) {
691 if (hash.indexOf("#") === -1) {
730 if (hash.indexOf("#") === -1) {
692 hash = "#" + hash;
731 hash = "#" + hash;
693 }
732 }
694 form.action = form.action + hash;
733 form.action = form.action + hash;
695 }
734 }
696 return true;
735 return true;
697 }
736 }
698
737
699 $(document).ready(setupAjaxIndicator);
738 $(document).ready(setupAjaxIndicator);
700 $(document).ready(hideOnLoad);
739 $(document).ready(hideOnLoad);
701 $(document).ready(addFormObserversForDoubleSubmit);
740 $(document).ready(addFormObserversForDoubleSubmit);
702 $(document).ready(defaultFocus);
741 $(document).ready(defaultFocus);
703 $(document).ready(setupTabs);
742 $(document).ready(setupTabs);
@@ -1,205 +1,205
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class TrackersControllerTest < ActionController::TestCase
20 class TrackersControllerTest < ActionController::TestCase
21 fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields, :issue_statuses
21 fixtures :trackers, :projects, :projects_trackers, :users, :issues, :custom_fields, :issue_statuses
22
22
23 def setup
23 def setup
24 User.current = nil
24 User.current = nil
25 @request.session[:user_id] = 1 # admin
25 @request.session[:user_id] = 1 # admin
26 end
26 end
27
27
28 def test_index
28 def test_index
29 get :index
29 get :index
30 assert_response :success
30 assert_response :success
31 assert_template 'index'
31 assert_template 'index'
32 end
32 end
33
33
34 def test_index_by_anonymous_should_redirect_to_login_form
34 def test_index_by_anonymous_should_redirect_to_login_form
35 @request.session[:user_id] = nil
35 @request.session[:user_id] = nil
36 get :index
36 get :index
37 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftrackers'
37 assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Ftrackers'
38 end
38 end
39
39
40 def test_index_by_user_should_respond_with_406
40 def test_index_by_user_should_respond_with_406
41 @request.session[:user_id] = 2
41 @request.session[:user_id] = 2
42 get :index
42 get :index
43 assert_response 406
43 assert_response 406
44 end
44 end
45
45
46 def test_new
46 def test_new
47 get :new
47 get :new
48 assert_response :success
48 assert_response :success
49 assert_template 'new'
49 assert_template 'new'
50 end
50 end
51
51
52 def test_create
52 def test_create
53 assert_difference 'Tracker.count' do
53 assert_difference 'Tracker.count' do
54 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1, :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
54 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1, :project_ids => ['1', '', ''], :custom_field_ids => ['1', '6', ''] }
55 end
55 end
56 assert_redirected_to :action => 'index'
56 assert_redirected_to :action => 'index'
57 tracker = Tracker.order('id DESC').first
57 tracker = Tracker.order('id DESC').first
58 assert_equal 'New tracker', tracker.name
58 assert_equal 'New tracker', tracker.name
59 assert_equal [1], tracker.project_ids.sort
59 assert_equal [1], tracker.project_ids.sort
60 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
60 assert_equal Tracker::CORE_FIELDS, tracker.core_fields
61 assert_equal [1, 6], tracker.custom_field_ids.sort
61 assert_equal [1, 6], tracker.custom_field_ids.sort
62 assert_equal 0, tracker.workflow_rules.count
62 assert_equal 0, tracker.workflow_rules.count
63 end
63 end
64
64
65 def test_create_with_disabled_core_fields
65 def test_create_with_disabled_core_fields
66 assert_difference 'Tracker.count' do
66 assert_difference 'Tracker.count' do
67 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1, :core_fields => ['assigned_to_id', 'fixed_version_id', ''] }
67 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1, :core_fields => ['assigned_to_id', 'fixed_version_id', ''] }
68 end
68 end
69 assert_redirected_to :action => 'index'
69 assert_redirected_to :action => 'index'
70 tracker = Tracker.order('id DESC').first
70 tracker = Tracker.order('id DESC').first
71 assert_equal 'New tracker', tracker.name
71 assert_equal 'New tracker', tracker.name
72 assert_equal %w(assigned_to_id fixed_version_id), tracker.core_fields
72 assert_equal %w(assigned_to_id fixed_version_id), tracker.core_fields
73 end
73 end
74
74
75 def test_create_new_with_workflow_copy
75 def test_create_new_with_workflow_copy
76 assert_difference 'Tracker.count' do
76 assert_difference 'Tracker.count' do
77 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1 }, :copy_workflow_from => 1
77 post :create, :tracker => { :name => 'New tracker', :default_status_id => 1 }, :copy_workflow_from => 1
78 end
78 end
79 assert_redirected_to :action => 'index'
79 assert_redirected_to :action => 'index'
80 tracker = Tracker.find_by_name('New tracker')
80 tracker = Tracker.find_by_name('New tracker')
81 assert_equal 0, tracker.projects.count
81 assert_equal 0, tracker.projects.count
82 assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count
82 assert_equal Tracker.find(1).workflow_rules.count, tracker.workflow_rules.count
83 end
83 end
84
84
85 def test_create_with_failure
85 def test_create_with_failure
86 assert_no_difference 'Tracker.count' do
86 assert_no_difference 'Tracker.count' do
87 post :create, :tracker => { :name => '', :project_ids => ['1', '', ''],
87 post :create, :tracker => { :name => '', :project_ids => ['1', '', ''],
88 :custom_field_ids => ['1', '6', ''] }
88 :custom_field_ids => ['1', '6', ''] }
89 end
89 end
90 assert_response :success
90 assert_response :success
91 assert_template 'new'
91 assert_template 'new'
92 assert_select_error /name cannot be blank/i
92 assert_select_error /name cannot be blank/i
93 end
93 end
94
94
95 def test_edit
95 def test_edit
96 Tracker.find(1).project_ids = [1, 3]
96 Tracker.find(1).project_ids = [1, 3]
97
97
98 get :edit, :id => 1
98 get :edit, :id => 1
99 assert_response :success
99 assert_response :success
100 assert_template 'edit'
100 assert_template 'edit'
101
101
102 assert_select 'input[name=?][value="1"][checked=checked]', 'tracker[project_ids][]'
102 assert_select 'input[name=?][value="1"][checked=checked]', 'tracker[project_ids][]'
103 assert_select 'input[name=?][value="2"]:not([checked])', 'tracker[project_ids][]'
103 assert_select 'input[name=?][value="2"]:not([checked])', 'tracker[project_ids][]'
104
104
105 assert_select 'input[name=?][value=""][type=hidden]', 'tracker[project_ids][]'
105 assert_select 'input[name=?][value=""][type=hidden]', 'tracker[project_ids][]'
106 end
106 end
107
107
108 def test_edit_should_check_core_fields
108 def test_edit_should_check_core_fields
109 tracker = Tracker.find(1)
109 tracker = Tracker.find(1)
110 tracker.core_fields = %w(assigned_to_id fixed_version_id)
110 tracker.core_fields = %w(assigned_to_id fixed_version_id)
111 tracker.save!
111 tracker.save!
112
112
113 get :edit, :id => 1
113 get :edit, :id => 1
114 assert_response :success
114 assert_response :success
115 assert_template 'edit'
115 assert_template 'edit'
116
116
117 assert_select 'input[name=?][value=assigned_to_id][checked=checked]', 'tracker[core_fields][]'
117 assert_select 'input[name=?][value=assigned_to_id][checked=checked]', 'tracker[core_fields][]'
118 assert_select 'input[name=?][value=fixed_version_id][checked=checked]', 'tracker[core_fields][]'
118 assert_select 'input[name=?][value=fixed_version_id][checked=checked]', 'tracker[core_fields][]'
119
119
120 assert_select 'input[name=?][value=category_id]', 'tracker[core_fields][]'
120 assert_select 'input[name=?][value=category_id]', 'tracker[core_fields][]'
121 assert_select 'input[name=?][value=category_id][checked=checked]', 'tracker[core_fields][]', 0
121 assert_select 'input[name=?][value=category_id][checked=checked]', 'tracker[core_fields][]', 0
122
122
123 assert_select 'input[name=?][value=""][type=hidden]', 'tracker[core_fields][]'
123 assert_select 'input[name=?][value=""][type=hidden]', 'tracker[core_fields][]'
124 end
124 end
125
125
126 def test_update
126 def test_update
127 put :update, :id => 1, :tracker => { :name => 'Renamed',
127 put :update, :id => 1, :tracker => { :name => 'Renamed',
128 :project_ids => ['1', '2', ''] }
128 :project_ids => ['1', '2', ''] }
129 assert_redirected_to :action => 'index'
129 assert_redirected_to :action => 'index'
130 assert_equal [1, 2], Tracker.find(1).project_ids.sort
130 assert_equal [1, 2], Tracker.find(1).project_ids.sort
131 end
131 end
132
132
133 def test_update_without_projects
133 def test_update_without_projects
134 put :update, :id => 1, :tracker => { :name => 'Renamed',
134 put :update, :id => 1, :tracker => { :name => 'Renamed',
135 :project_ids => [''] }
135 :project_ids => [''] }
136 assert_redirected_to :action => 'index'
136 assert_redirected_to :action => 'index'
137 assert Tracker.find(1).project_ids.empty?
137 assert Tracker.find(1).project_ids.empty?
138 end
138 end
139
139
140 def test_update_without_core_fields
140 def test_update_without_core_fields
141 put :update, :id => 1, :tracker => { :name => 'Renamed', :core_fields => [''] }
141 put :update, :id => 1, :tracker => { :name => 'Renamed', :core_fields => [''] }
142 assert_redirected_to :action => 'index'
142 assert_redirected_to :action => 'index'
143 assert Tracker.find(1).core_fields.empty?
143 assert Tracker.find(1).core_fields.empty?
144 end
144 end
145
145
146 def test_update_with_failure
146 def test_update_with_failure
147 put :update, :id => 1, :tracker => { :name => '' }
147 put :update, :id => 1, :tracker => { :name => '' }
148 assert_response :success
148 assert_response :success
149 assert_template 'edit'
149 assert_template 'edit'
150 assert_select_error /name cannot be blank/i
150 assert_select_error /name cannot be blank/i
151 end
151 end
152
152
153 def test_move_lower
153 def test_move_lower
154 tracker = Tracker.find_by_position(1)
154 tracker = Tracker.find_by_position(1)
155 put :update, :id => 1, :tracker => { :move_to => 'lower' }
155 put :update, :id => 1, :tracker => { :position => '2' }
156 assert_equal 2, tracker.reload.position
156 assert_equal 2, tracker.reload.position
157 end
157 end
158
158
159 def test_destroy
159 def test_destroy
160 tracker = Tracker.generate!(:name => 'Destroyable')
160 tracker = Tracker.generate!(:name => 'Destroyable')
161 assert_difference 'Tracker.count', -1 do
161 assert_difference 'Tracker.count', -1 do
162 delete :destroy, :id => tracker.id
162 delete :destroy, :id => tracker.id
163 end
163 end
164 assert_redirected_to :action => 'index'
164 assert_redirected_to :action => 'index'
165 assert_nil flash[:error]
165 assert_nil flash[:error]
166 end
166 end
167
167
168 def test_destroy_tracker_in_use
168 def test_destroy_tracker_in_use
169 assert_no_difference 'Tracker.count' do
169 assert_no_difference 'Tracker.count' do
170 delete :destroy, :id => 1
170 delete :destroy, :id => 1
171 end
171 end
172 assert_redirected_to :action => 'index'
172 assert_redirected_to :action => 'index'
173 assert_not_nil flash[:error]
173 assert_not_nil flash[:error]
174 end
174 end
175
175
176 def test_get_fields
176 def test_get_fields
177 get :fields
177 get :fields
178 assert_response :success
178 assert_response :success
179 assert_template 'fields'
179 assert_template 'fields'
180
180
181 assert_select 'form' do
181 assert_select 'form' do
182 assert_select 'input[type=checkbox][name=?][value=assigned_to_id]', 'trackers[1][core_fields][]'
182 assert_select 'input[type=checkbox][name=?][value=assigned_to_id]', 'trackers[1][core_fields][]'
183 assert_select 'input[type=checkbox][name=?][value="2"]', 'trackers[1][custom_field_ids][]'
183 assert_select 'input[type=checkbox][name=?][value="2"]', 'trackers[1][custom_field_ids][]'
184
184
185 assert_select 'input[type=hidden][name=?][value=""]', 'trackers[1][core_fields][]'
185 assert_select 'input[type=hidden][name=?][value=""]', 'trackers[1][core_fields][]'
186 assert_select 'input[type=hidden][name=?][value=""]', 'trackers[1][custom_field_ids][]'
186 assert_select 'input[type=hidden][name=?][value=""]', 'trackers[1][custom_field_ids][]'
187 end
187 end
188 end
188 end
189
189
190 def test_post_fields
190 def test_post_fields
191 post :fields, :trackers => {
191 post :fields, :trackers => {
192 '1' => {'core_fields' => ['assigned_to_id', 'due_date', ''], 'custom_field_ids' => ['1', '2']},
192 '1' => {'core_fields' => ['assigned_to_id', 'due_date', ''], 'custom_field_ids' => ['1', '2']},
193 '2' => {'core_fields' => [''], 'custom_field_ids' => ['']}
193 '2' => {'core_fields' => [''], 'custom_field_ids' => ['']}
194 }
194 }
195 assert_redirected_to '/trackers/fields'
195 assert_redirected_to '/trackers/fields'
196
196
197 tracker = Tracker.find(1)
197 tracker = Tracker.find(1)
198 assert_equal %w(assigned_to_id due_date), tracker.core_fields
198 assert_equal %w(assigned_to_id due_date), tracker.core_fields
199 assert_equal [1, 2], tracker.custom_field_ids.sort
199 assert_equal [1, 2], tracker.custom_field_ids.sort
200
200
201 tracker = Tracker.find(2)
201 tracker = Tracker.find(2)
202 assert_equal [], tracker.core_fields
202 assert_equal [], tracker.core_fields
203 assert_equal [], tracker.custom_field_ids.sort
203 assert_equal [], tracker.custom_field_ids.sort
204 end
204 end
205 end
205 end
General Comments 0
You need to be logged in to leave comments. Login now