##// END OF EJS Templates
Adds file custom field format (#6719)....
Jean-Philippe Lang -
r15535:ef45304817e9
parent child
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,156
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
20 class AttachmentFieldFormatTest < Redmine::IntegrationTest
21 fixtures :projects,
22 :users, :email_addresses,
23 :roles,
24 :members,
25 :member_roles,
26 :trackers,
27 :projects_trackers,
28 :enabled_modules,
29 :issue_statuses,
30 :issues,
31 :enumerations,
32 :custom_fields,
33 :custom_values,
34 :custom_fields_trackers,
35 :attachments
36
37 def setup
38 set_tmp_attachments_directory
39 @field = IssueCustomField.generate!(:name => "File", :field_format => "attachment")
40 log_user "jsmith", "jsmith"
41 end
42
43 def test_new_should_include_inputs
44 get '/projects/ecookbook/issues/new'
45 assert_response :success
46
47 assert_select '[name^=?]', "issue[custom_field_values][#{@field.id}]", 2
48 assert_select 'input[name=?][type=hidden][value=""]', "issue[custom_field_values][#{@field.id}][blank]"
49 end
50
51 def test_create_with_attachment
52 issue = new_record(Issue) do
53 assert_difference 'Attachment.count' do
54 post '/projects/ecookbook/issues', {
55 :issue => {
56 :subject => "Subject",
57 :custom_field_values => {
58 @field.id => {
59 'blank' => '',
60 '1' => {:file => uploaded_test_file("testfile.txt", "text/plain")}
61 }
62 }
63 }
64 }
65 assert_response 302
66 end
67 end
68
69 custom_value = issue.custom_value_for(@field)
70 assert custom_value
71 assert custom_value.value.present?
72
73 attachment = Attachment.find_by_id(custom_value.value)
74 assert attachment
75 assert_equal custom_value, attachment.container
76
77 follow_redirect!
78 assert_response :success
79
80 # link to the attachment
81 link = css_select(".cf_#{@field.id} .value a")
82 assert_equal 1, link.size
83 assert_equal "testfile.txt", link.text
84
85 # download the attachment
86 get link.attr('href')
87 assert_response :success
88 assert_equal "text/plain", response.content_type
89 end
90
91 def test_create_without_attachment
92 issue = new_record(Issue) do
93 assert_no_difference 'Attachment.count' do
94 post '/projects/ecookbook/issues', {
95 :issue => {
96 :subject => "Subject",
97 :custom_field_values => {
98 @field.id => {:blank => ''}
99 }
100 }
101 }
102 assert_response 302
103 end
104 end
105
106 custom_value = issue.custom_value_for(@field)
107 assert custom_value
108 assert custom_value.value.blank?
109
110 follow_redirect!
111 assert_response :success
112
113 # no links to the attachment
114 assert_select ".cf_#{@field.id} .value a", 0
115 end
116
117 def test_failure_on_create_should_preserve_attachment
118 attachment = new_record(Attachment) do
119 assert_no_difference 'Issue.count' do
120 post '/projects/ecookbook/issues', {
121 :issue => {
122 :subject => "",
123 :custom_field_values => {
124 @field.id => {:file => uploaded_test_file("testfile.txt", "text/plain")}
125 }
126 }
127 }
128 assert_response :success
129 assert_select_error /Subject cannot be blank/
130 end
131 end
132
133 assert_nil attachment.container_id
134 assert_select 'input[name=?][value=?][type=hidden]', "issue[custom_field_values][#{@field.id}][p0][token]", attachment.token
135 assert_select 'input[name=?][value=?]', "issue[custom_field_values][#{@field.id}][p0][filename]", 'testfile.txt'
136
137 issue = new_record(Issue) do
138 assert_no_difference 'Attachment.count' do
139 post '/projects/ecookbook/issues', {
140 :issue => {
141 :subject => "Subject",
142 :custom_field_values => {
143 @field.id => {:token => attachment.token}
144 }
145 }
146 }
147 assert_response 302
148 end
149 end
150
151 custom_value = issue.custom_value_for(@field)
152 assert custom_value
153 assert_equal attachment.id.to_s, custom_value.value
154 assert_equal custom_value, attachment.reload.container
155 end
156 end
@@ -0,0 +1,163
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
19 require 'redmine/field_format'
20
21 class Redmine::AttachmentFieldFormatTest < ActionView::TestCase
22 include ApplicationHelper
23 include Redmine::I18n
24
25 fixtures :users
26
27 def setup
28 set_language_if_valid 'en'
29 set_tmp_attachments_directory
30 end
31
32 def test_should_accept_a_hash_with_upload_on_create
33 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
34 group = Group.new(:name => 'Group')
35 attachment = nil
36
37 custom_value = new_record(CustomValue) do
38 attachment = new_record(Attachment) do
39 group.custom_field_values = {field.id => {:file => mock_file}}
40 assert group.save
41 end
42 end
43
44 assert_equal 'a_file.png', attachment.filename
45 assert_equal custom_value, attachment.container
46 assert_equal field, attachment.container.custom_field
47 assert_equal group, attachment.container.customized
48 end
49
50 def test_should_accept_a_hash_with_no_upload_on_create
51 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
52 group = Group.new(:name => 'Group')
53 attachment = nil
54
55 custom_value = new_record(CustomValue) do
56 assert_no_difference 'Attachment.count' do
57 group.custom_field_values = {field.id => {}}
58 assert group.save
59 end
60 end
61
62 assert_equal '', custom_value.value
63 end
64
65 def test_should_not_validate_with_invalid_upload_on_create
66 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
67 group = Group.new(:name => 'Group')
68
69 with_settings :attachment_max_size => 0 do
70 assert_no_difference 'CustomValue.count' do
71 assert_no_difference 'Attachment.count' do
72 group.custom_field_values = {field.id => {:file => mock_file}}
73 assert_equal false, group.save
74 end
75 end
76 end
77 end
78
79 def test_should_accept_a_hash_with_token_on_create
80 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
81 group = Group.new(:name => 'Group')
82
83 attachment = Attachment.create!(:file => mock_file, :author => User.find(2))
84 assert_nil attachment.container
85
86 custom_value = new_record(CustomValue) do
87 assert_no_difference 'Attachment.count' do
88 group.custom_field_values = {field.id => {:token => attachment.token}}
89 assert group.save
90 end
91 end
92
93 attachment.reload
94 assert_equal custom_value, attachment.container
95 assert_equal field, attachment.container.custom_field
96 assert_equal group, attachment.container.customized
97 end
98
99 def test_should_not_validate_with_invalid_token_on_create
100 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
101 group = Group.new(:name => 'Group')
102
103 assert_no_difference 'CustomValue.count' do
104 assert_no_difference 'Attachment.count' do
105 group.custom_field_values = {field.id => {:token => "123.0123456789abcdef"}}
106 assert_equal false, group.save
107 end
108 end
109 end
110
111 def test_should_replace_attachment_on_update
112 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
113 group = Group.new(:name => 'Group')
114 attachment = nil
115 custom_value = new_record(CustomValue) do
116 attachment = new_record(Attachment) do
117 group.custom_field_values = {field.id => {:file => mock_file}}
118 assert group.save
119 end
120 end
121 group.reload
122
123 assert_no_difference 'Attachment.count' do
124 assert_no_difference 'CustomValue.count' do
125 group.custom_field_values = {field.id => {:file => mock_file}}
126 assert group.save
127 end
128 end
129
130 assert !Attachment.exists?(attachment.id)
131 assert CustomValue.exists?(custom_value.id)
132
133 new_attachment = Attachment.order(:id => :desc).first
134 custom_value.reload
135 assert_equal custom_value, new_attachment.container
136 end
137
138 def test_should_delete_attachment_on_update
139 field = GroupCustomField.generate!(:name => "File", :field_format => 'attachment')
140 group = Group.new(:name => 'Group')
141 attachment = nil
142 custom_value = new_record(CustomValue) do
143 attachment = new_record(Attachment) do
144 group.custom_field_values = {field.id => {:file => mock_file}}
145 assert group.save
146 end
147 end
148 group.reload
149
150 assert_difference 'Attachment.count', -1 do
151 assert_no_difference 'CustomValue.count' do
152 group.custom_field_values = {field.id => {}}
153 assert group.save
154 end
155 end
156
157 assert !Attachment.exists?(attachment.id)
158 assert CustomValue.exists?(custom_value.id)
159
160 custom_value.reload
161 assert_equal '', custom_value.value
162 end
163 end
@@ -1,1372 +1,1374
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 include Redmine::Helpers::URL
31 include Redmine::Helpers::URL
32
32
33 extend Forwardable
33 extend Forwardable
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
35
35
36 # Return true if user is authorized for controller/action, otherwise false
36 # Return true if user is authorized for controller/action, otherwise false
37 def authorize_for(controller, action)
37 def authorize_for(controller, action)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
39 end
39 end
40
40
41 # Display a link if user is authorized
41 # Display a link if user is authorized
42 #
42 #
43 # @param [String] name Anchor text (passed to link_to)
43 # @param [String] name Anchor text (passed to link_to)
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
45 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] html_options Options passed to link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
49 end
49 end
50
50
51 # Displays a link to user's account page if active
51 # Displays a link to user's account page if active
52 def link_to_user(user, options={})
52 def link_to_user(user, options={})
53 if user.is_a?(User)
53 if user.is_a?(User)
54 name = h(user.name(options[:format]))
54 name = h(user.name(options[:format]))
55 if user.active? || (User.current.admin? && user.logged?)
55 if user.active? || (User.current.admin? && user.logged?)
56 link_to name, user_path(user), :class => user.css_classes
56 link_to name, user_path(user), :class => user.css_classes
57 else
57 else
58 name
58 name
59 end
59 end
60 else
60 else
61 h(user.to_s)
61 h(user.to_s)
62 end
62 end
63 end
63 end
64
64
65 # Displays a link to +issue+ with its subject.
65 # Displays a link to +issue+ with its subject.
66 # Examples:
66 # Examples:
67 #
67 #
68 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue) # => Defect #6: This is the subject
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
70 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :subject => false) # => Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
73 #
73 #
74 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
75 title = nil
75 title = nil
76 subject = nil
76 subject = nil
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
78 if options[:subject] == false
78 if options[:subject] == false
79 title = issue.subject.truncate(60)
79 title = issue.subject.truncate(60)
80 else
80 else
81 subject = issue.subject
81 subject = issue.subject
82 if truncate_length = options[:truncate]
82 if truncate_length = options[:truncate]
83 subject = subject.truncate(truncate_length)
83 subject = subject.truncate(truncate_length)
84 end
84 end
85 end
85 end
86 only_path = options[:only_path].nil? ? true : options[:only_path]
86 only_path = options[:only_path].nil? ? true : options[:only_path]
87 s = link_to(text, issue_url(issue, :only_path => only_path),
87 s = link_to(text, issue_url(issue, :only_path => only_path),
88 :class => issue.css_classes, :title => title)
88 :class => issue.css_classes, :title => title)
89 s << h(": #{subject}") if subject
89 s << h(": #{subject}") if subject
90 s = h("#{issue.project} - ") + s if options[:project]
90 s = h("#{issue.project} - ") + s if options[:project]
91 s
91 s
92 end
92 end
93
93
94 # Generates a link to an attachment.
94 # Generates a link to an attachment.
95 # Options:
95 # Options:
96 # * :text - Link text (default to attachment filename)
96 # * :text - Link text (default to attachment filename)
97 # * :download - Force download (default: false)
97 # * :download - Force download (default: false)
98 def link_to_attachment(attachment, options={})
98 def link_to_attachment(attachment, options={})
99 text = options.delete(:text) || attachment.filename
99 text = options.delete(:text) || attachment.filename
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
101 html_options = options.slice!(:only_path)
101 html_options = options.slice!(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
103 url = send(route_method, attachment, attachment.filename, options)
103 url = send(route_method, attachment, attachment.filename, options)
104 link_to text, url, html_options
104 link_to text, url, html_options
105 end
105 end
106
106
107 # Generates a link to a SCM revision
107 # Generates a link to a SCM revision
108 # Options:
108 # Options:
109 # * :text - Link text (default to the formatted revision)
109 # * :text - Link text (default to the formatted revision)
110 def link_to_revision(revision, repository, options={})
110 def link_to_revision(revision, repository, options={})
111 if repository.is_a?(Project)
111 if repository.is_a?(Project)
112 repository = repository.repository
112 repository = repository.repository
113 end
113 end
114 text = options.delete(:text) || format_revision(revision)
114 text = options.delete(:text) || format_revision(revision)
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
116 link_to(
116 link_to(
117 h(text),
117 h(text),
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
119 :title => l(:label_revision_id, format_revision(revision)),
119 :title => l(:label_revision_id, format_revision(revision)),
120 :accesskey => options[:accesskey]
120 :accesskey => options[:accesskey]
121 )
121 )
122 end
122 end
123
123
124 # Generates a link to a message
124 # Generates a link to a message
125 def link_to_message(message, options={}, html_options = nil)
125 def link_to_message(message, options={}, html_options = nil)
126 link_to(
126 link_to(
127 message.subject.truncate(60),
127 message.subject.truncate(60),
128 board_message_url(message.board_id, message.parent_id || message.id, {
128 board_message_url(message.board_id, message.parent_id || message.id, {
129 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
131 :only_path => true
131 :only_path => true
132 }.merge(options)),
132 }.merge(options)),
133 html_options
133 html_options
134 )
134 )
135 end
135 end
136
136
137 # Generates a link to a project if active
137 # Generates a link to a project if active
138 # Examples:
138 # Examples:
139 #
139 #
140 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project) # => link to the specified project overview
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
143 #
143 #
144 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
145 if project.archived?
145 if project.archived?
146 h(project.name)
146 h(project.name)
147 else
147 else
148 link_to project.name,
148 link_to project.name,
149 project_url(project, {:only_path => true}.merge(options)),
149 project_url(project, {:only_path => true}.merge(options)),
150 html_options
150 html_options
151 end
151 end
152 end
152 end
153
153
154 # Generates a link to a project settings if active
154 # Generates a link to a project settings if active
155 def link_to_project_settings(project, options={}, html_options=nil)
155 def link_to_project_settings(project, options={}, html_options=nil)
156 if project.active?
156 if project.active?
157 link_to project.name, settings_project_path(project, options), html_options
157 link_to project.name, settings_project_path(project, options), html_options
158 elsif project.archived?
158 elsif project.archived?
159 h(project.name)
159 h(project.name)
160 else
160 else
161 link_to project.name, project_path(project, options), html_options
161 link_to project.name, project_path(project, options), html_options
162 end
162 end
163 end
163 end
164
164
165 # Generates a link to a version
165 # Generates a link to a version
166 def link_to_version(version, options = {})
166 def link_to_version(version, options = {})
167 return '' unless version && version.is_a?(Version)
167 return '' unless version && version.is_a?(Version)
168 options = {:title => format_date(version.effective_date)}.merge(options)
168 options = {:title => format_date(version.effective_date)}.merge(options)
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
170 end
170 end
171
171
172 # Helper that formats object for html or text rendering
172 # Helper that formats object for html or text rendering
173 def format_object(object, html=true, &block)
173 def format_object(object, html=true, &block)
174 if block_given?
174 if block_given?
175 object = yield object
175 object = yield object
176 end
176 end
177 case object.class.name
177 case object.class.name
178 when 'Array'
178 when 'Array'
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
180 when 'Time'
180 when 'Time'
181 format_time(object)
181 format_time(object)
182 when 'Date'
182 when 'Date'
183 format_date(object)
183 format_date(object)
184 when 'Fixnum'
184 when 'Fixnum'
185 object.to_s
185 object.to_s
186 when 'Float'
186 when 'Float'
187 sprintf "%.2f", object
187 sprintf "%.2f", object
188 when 'User'
188 when 'User'
189 html ? link_to_user(object) : object.to_s
189 html ? link_to_user(object) : object.to_s
190 when 'Project'
190 when 'Project'
191 html ? link_to_project(object) : object.to_s
191 html ? link_to_project(object) : object.to_s
192 when 'Version'
192 when 'Version'
193 html ? link_to_version(object) : object.to_s
193 html ? link_to_version(object) : object.to_s
194 when 'TrueClass'
194 when 'TrueClass'
195 l(:general_text_Yes)
195 l(:general_text_Yes)
196 when 'FalseClass'
196 when 'FalseClass'
197 l(:general_text_No)
197 l(:general_text_No)
198 when 'Issue'
198 when 'Issue'
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
200 when 'Attachment'
201 html ? link_to_attachment(object, :download => true) : object.filename
200 when 'CustomValue', 'CustomFieldValue'
202 when 'CustomValue', 'CustomFieldValue'
201 if object.custom_field
203 if object.custom_field
202 f = object.custom_field.format.formatted_custom_value(self, object, html)
204 f = object.custom_field.format.formatted_custom_value(self, object, html)
203 if f.nil? || f.is_a?(String)
205 if f.nil? || f.is_a?(String)
204 f
206 f
205 else
207 else
206 format_object(f, html, &block)
208 format_object(f, html, &block)
207 end
209 end
208 else
210 else
209 object.value.to_s
211 object.value.to_s
210 end
212 end
211 else
213 else
212 html ? h(object) : object.to_s
214 html ? h(object) : object.to_s
213 end
215 end
214 end
216 end
215
217
216 def wiki_page_path(page, options={})
218 def wiki_page_path(page, options={})
217 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
219 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
218 end
220 end
219
221
220 def thumbnail_tag(attachment)
222 def thumbnail_tag(attachment)
221 link_to image_tag(thumbnail_path(attachment)),
223 link_to image_tag(thumbnail_path(attachment)),
222 named_attachment_path(attachment, attachment.filename),
224 named_attachment_path(attachment, attachment.filename),
223 :title => attachment.filename
225 :title => attachment.filename
224 end
226 end
225
227
226 def toggle_link(name, id, options={})
228 def toggle_link(name, id, options={})
227 onclick = "$('##{id}').toggle(); "
229 onclick = "$('##{id}').toggle(); "
228 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
230 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
229 onclick << "return false;"
231 onclick << "return false;"
230 link_to(name, "#", :onclick => onclick)
232 link_to(name, "#", :onclick => onclick)
231 end
233 end
232
234
233 # Used to format item titles on the activity view
235 # Used to format item titles on the activity view
234 def format_activity_title(text)
236 def format_activity_title(text)
235 text
237 text
236 end
238 end
237
239
238 def format_activity_day(date)
240 def format_activity_day(date)
239 date == User.current.today ? l(:label_today).titleize : format_date(date)
241 date == User.current.today ? l(:label_today).titleize : format_date(date)
240 end
242 end
241
243
242 def format_activity_description(text)
244 def format_activity_description(text)
243 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
245 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
244 ).gsub(/[\r\n]+/, "<br />").html_safe
246 ).gsub(/[\r\n]+/, "<br />").html_safe
245 end
247 end
246
248
247 def format_version_name(version)
249 def format_version_name(version)
248 if version.project == @project
250 if version.project == @project
249 h(version)
251 h(version)
250 else
252 else
251 h("#{version.project} - #{version}")
253 h("#{version.project} - #{version}")
252 end
254 end
253 end
255 end
254
256
255 def due_date_distance_in_words(date)
257 def due_date_distance_in_words(date)
256 if date
258 if date
257 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
259 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
258 end
260 end
259 end
261 end
260
262
261 # Renders a tree of projects as a nested set of unordered lists
263 # Renders a tree of projects as a nested set of unordered lists
262 # The given collection may be a subset of the whole project tree
264 # The given collection may be a subset of the whole project tree
263 # (eg. some intermediate nodes are private and can not be seen)
265 # (eg. some intermediate nodes are private and can not be seen)
264 def render_project_nested_lists(projects, &block)
266 def render_project_nested_lists(projects, &block)
265 s = ''
267 s = ''
266 if projects.any?
268 if projects.any?
267 ancestors = []
269 ancestors = []
268 original_project = @project
270 original_project = @project
269 projects.sort_by(&:lft).each do |project|
271 projects.sort_by(&:lft).each do |project|
270 # set the project environment to please macros.
272 # set the project environment to please macros.
271 @project = project
273 @project = project
272 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
274 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
273 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
275 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
274 else
276 else
275 ancestors.pop
277 ancestors.pop
276 s << "</li>"
278 s << "</li>"
277 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
279 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
278 ancestors.pop
280 ancestors.pop
279 s << "</ul></li>\n"
281 s << "</ul></li>\n"
280 end
282 end
281 end
283 end
282 classes = (ancestors.empty? ? 'root' : 'child')
284 classes = (ancestors.empty? ? 'root' : 'child')
283 s << "<li class='#{classes}'><div class='#{classes}'>"
285 s << "<li class='#{classes}'><div class='#{classes}'>"
284 s << h(block_given? ? capture(project, &block) : project.name)
286 s << h(block_given? ? capture(project, &block) : project.name)
285 s << "</div>\n"
287 s << "</div>\n"
286 ancestors << project
288 ancestors << project
287 end
289 end
288 s << ("</li></ul>\n" * ancestors.size)
290 s << ("</li></ul>\n" * ancestors.size)
289 @project = original_project
291 @project = original_project
290 end
292 end
291 s.html_safe
293 s.html_safe
292 end
294 end
293
295
294 def render_page_hierarchy(pages, node=nil, options={})
296 def render_page_hierarchy(pages, node=nil, options={})
295 content = ''
297 content = ''
296 if pages[node]
298 if pages[node]
297 content << "<ul class=\"pages-hierarchy\">\n"
299 content << "<ul class=\"pages-hierarchy\">\n"
298 pages[node].each do |page|
300 pages[node].each do |page|
299 content << "<li>"
301 content << "<li>"
300 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
302 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
301 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
303 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
302 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
304 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
303 content << "</li>\n"
305 content << "</li>\n"
304 end
306 end
305 content << "</ul>\n"
307 content << "</ul>\n"
306 end
308 end
307 content.html_safe
309 content.html_safe
308 end
310 end
309
311
310 # Renders flash messages
312 # Renders flash messages
311 def render_flash_messages
313 def render_flash_messages
312 s = ''
314 s = ''
313 flash.each do |k,v|
315 flash.each do |k,v|
314 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
316 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
315 end
317 end
316 s.html_safe
318 s.html_safe
317 end
319 end
318
320
319 # Renders tabs and their content
321 # Renders tabs and their content
320 def render_tabs(tabs, selected=params[:tab])
322 def render_tabs(tabs, selected=params[:tab])
321 if tabs.any?
323 if tabs.any?
322 unless tabs.detect {|tab| tab[:name] == selected}
324 unless tabs.detect {|tab| tab[:name] == selected}
323 selected = nil
325 selected = nil
324 end
326 end
325 selected ||= tabs.first[:name]
327 selected ||= tabs.first[:name]
326 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
328 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
327 else
329 else
328 content_tag 'p', l(:label_no_data), :class => "nodata"
330 content_tag 'p', l(:label_no_data), :class => "nodata"
329 end
331 end
330 end
332 end
331
333
332 # Renders the project quick-jump box
334 # Renders the project quick-jump box
333 def render_project_jump_box
335 def render_project_jump_box
334 return unless User.current.logged?
336 return unless User.current.logged?
335 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
337 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
336 if projects.any?
338 if projects.any?
337 options =
339 options =
338 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
340 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
339 '<option value="" disabled="disabled">---</option>').html_safe
341 '<option value="" disabled="disabled">---</option>').html_safe
340
342
341 options << project_tree_options_for_select(projects, :selected => @project) do |p|
343 options << project_tree_options_for_select(projects, :selected => @project) do |p|
342 { :value => project_path(:id => p, :jump => current_menu_item) }
344 { :value => project_path(:id => p, :jump => current_menu_item) }
343 end
345 end
344
346
345 content_tag( :span, nil, :class => 'jump-box-arrow') +
347 content_tag( :span, nil, :class => 'jump-box-arrow') +
346 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
348 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
347 end
349 end
348 end
350 end
349
351
350 def project_tree_options_for_select(projects, options = {})
352 def project_tree_options_for_select(projects, options = {})
351 s = ''.html_safe
353 s = ''.html_safe
352 if blank_text = options[:include_blank]
354 if blank_text = options[:include_blank]
353 if blank_text == true
355 if blank_text == true
354 blank_text = '&nbsp;'.html_safe
356 blank_text = '&nbsp;'.html_safe
355 end
357 end
356 s << content_tag('option', blank_text, :value => '')
358 s << content_tag('option', blank_text, :value => '')
357 end
359 end
358 project_tree(projects) do |project, level|
360 project_tree(projects) do |project, level|
359 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
361 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
360 tag_options = {:value => project.id}
362 tag_options = {:value => project.id}
361 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
363 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
362 tag_options[:selected] = 'selected'
364 tag_options[:selected] = 'selected'
363 else
365 else
364 tag_options[:selected] = nil
366 tag_options[:selected] = nil
365 end
367 end
366 tag_options.merge!(yield(project)) if block_given?
368 tag_options.merge!(yield(project)) if block_given?
367 s << content_tag('option', name_prefix + h(project), tag_options)
369 s << content_tag('option', name_prefix + h(project), tag_options)
368 end
370 end
369 s.html_safe
371 s.html_safe
370 end
372 end
371
373
372 # Yields the given block for each project with its level in the tree
374 # Yields the given block for each project with its level in the tree
373 #
375 #
374 # Wrapper for Project#project_tree
376 # Wrapper for Project#project_tree
375 def project_tree(projects, options={}, &block)
377 def project_tree(projects, options={}, &block)
376 Project.project_tree(projects, options, &block)
378 Project.project_tree(projects, options, &block)
377 end
379 end
378
380
379 def principals_check_box_tags(name, principals)
381 def principals_check_box_tags(name, principals)
380 s = ''
382 s = ''
381 principals.each do |principal|
383 principals.each do |principal|
382 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
384 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
383 end
385 end
384 s.html_safe
386 s.html_safe
385 end
387 end
386
388
387 # Returns a string for users/groups option tags
389 # Returns a string for users/groups option tags
388 def principals_options_for_select(collection, selected=nil)
390 def principals_options_for_select(collection, selected=nil)
389 s = ''
391 s = ''
390 if collection.include?(User.current)
392 if collection.include?(User.current)
391 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
393 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
392 end
394 end
393 groups = ''
395 groups = ''
394 collection.sort.each do |element|
396 collection.sort.each do |element|
395 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
397 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
396 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
398 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
397 end
399 end
398 unless groups.empty?
400 unless groups.empty?
399 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
401 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
400 end
402 end
401 s.html_safe
403 s.html_safe
402 end
404 end
403
405
404 def option_tag(name, text, value, selected=nil, options={})
406 def option_tag(name, text, value, selected=nil, options={})
405 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
407 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
406 end
408 end
407
409
408 def truncate_single_line_raw(string, length)
410 def truncate_single_line_raw(string, length)
409 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
411 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
410 end
412 end
411
413
412 # Truncates at line break after 250 characters or options[:length]
414 # Truncates at line break after 250 characters or options[:length]
413 def truncate_lines(string, options={})
415 def truncate_lines(string, options={})
414 length = options[:length] || 250
416 length = options[:length] || 250
415 if string.to_s =~ /\A(.{#{length}}.*?)$/m
417 if string.to_s =~ /\A(.{#{length}}.*?)$/m
416 "#{$1}..."
418 "#{$1}..."
417 else
419 else
418 string
420 string
419 end
421 end
420 end
422 end
421
423
422 def anchor(text)
424 def anchor(text)
423 text.to_s.gsub(' ', '_')
425 text.to_s.gsub(' ', '_')
424 end
426 end
425
427
426 def html_hours(text)
428 def html_hours(text)
427 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
429 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
428 end
430 end
429
431
430 def authoring(created, author, options={})
432 def authoring(created, author, options={})
431 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
433 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
432 end
434 end
433
435
434 def time_tag(time)
436 def time_tag(time)
435 text = distance_of_time_in_words(Time.now, time)
437 text = distance_of_time_in_words(Time.now, time)
436 if @project
438 if @project
437 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
439 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
438 else
440 else
439 content_tag('abbr', text, :title => format_time(time))
441 content_tag('abbr', text, :title => format_time(time))
440 end
442 end
441 end
443 end
442
444
443 def syntax_highlight_lines(name, content)
445 def syntax_highlight_lines(name, content)
444 lines = []
446 lines = []
445 syntax_highlight(name, content).each_line { |line| lines << line }
447 syntax_highlight(name, content).each_line { |line| lines << line }
446 lines
448 lines
447 end
449 end
448
450
449 def syntax_highlight(name, content)
451 def syntax_highlight(name, content)
450 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
452 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
451 end
453 end
452
454
453 def to_path_param(path)
455 def to_path_param(path)
454 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
456 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
455 str.blank? ? nil : str
457 str.blank? ? nil : str
456 end
458 end
457
459
458 def reorder_links(name, url, method = :post)
460 def reorder_links(name, url, method = :post)
459 # TODO: remove associated styles from application.css too
461 # TODO: remove associated styles from application.css too
460 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
462 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
461
463
462 link_to(l(:label_sort_highest),
464 link_to(l(:label_sort_highest),
463 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
465 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
464 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
466 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
465 link_to(l(:label_sort_higher),
467 link_to(l(:label_sort_higher),
466 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
468 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
467 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
469 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
468 link_to(l(:label_sort_lower),
470 link_to(l(:label_sort_lower),
469 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
471 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
470 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
472 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
471 link_to(l(:label_sort_lowest),
473 link_to(l(:label_sort_lowest),
472 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
474 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
473 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
475 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
474 end
476 end
475
477
476 def reorder_handle(object, options={})
478 def reorder_handle(object, options={})
477 data = {
479 data = {
478 :reorder_url => options[:url] || url_for(object),
480 :reorder_url => options[:url] || url_for(object),
479 :reorder_param => options[:param] || object.class.name.underscore
481 :reorder_param => options[:param] || object.class.name.underscore
480 }
482 }
481 content_tag('span', '',
483 content_tag('span', '',
482 :class => "sort-handle",
484 :class => "sort-handle",
483 :data => data,
485 :data => data,
484 :title => l(:button_sort))
486 :title => l(:button_sort))
485 end
487 end
486
488
487 def breadcrumb(*args)
489 def breadcrumb(*args)
488 elements = args.flatten
490 elements = args.flatten
489 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
491 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
490 end
492 end
491
493
492 def other_formats_links(&block)
494 def other_formats_links(&block)
493 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
495 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
494 yield Redmine::Views::OtherFormatsBuilder.new(self)
496 yield Redmine::Views::OtherFormatsBuilder.new(self)
495 concat('</p>'.html_safe)
497 concat('</p>'.html_safe)
496 end
498 end
497
499
498 def page_header_title
500 def page_header_title
499 if @project.nil? || @project.new_record?
501 if @project.nil? || @project.new_record?
500 h(Setting.app_title)
502 h(Setting.app_title)
501 else
503 else
502 b = []
504 b = []
503 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
505 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
504 if ancestors.any?
506 if ancestors.any?
505 root = ancestors.shift
507 root = ancestors.shift
506 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
508 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
507 if ancestors.size > 2
509 if ancestors.size > 2
508 b << "\xe2\x80\xa6"
510 b << "\xe2\x80\xa6"
509 ancestors = ancestors[-2, 2]
511 ancestors = ancestors[-2, 2]
510 end
512 end
511 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
513 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
512 end
514 end
513 b << content_tag(:span, h(@project), class: 'current-project')
515 b << content_tag(:span, h(@project), class: 'current-project')
514 if b.size > 1
516 if b.size > 1
515 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
517 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
516 path = safe_join(b[0..-2], separator) + separator
518 path = safe_join(b[0..-2], separator) + separator
517 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
519 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
518 end
520 end
519 safe_join b
521 safe_join b
520 end
522 end
521 end
523 end
522
524
523 # Returns a h2 tag and sets the html title with the given arguments
525 # Returns a h2 tag and sets the html title with the given arguments
524 def title(*args)
526 def title(*args)
525 strings = args.map do |arg|
527 strings = args.map do |arg|
526 if arg.is_a?(Array) && arg.size >= 2
528 if arg.is_a?(Array) && arg.size >= 2
527 link_to(*arg)
529 link_to(*arg)
528 else
530 else
529 h(arg.to_s)
531 h(arg.to_s)
530 end
532 end
531 end
533 end
532 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
534 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
533 content_tag('h2', strings.join(' &#187; ').html_safe)
535 content_tag('h2', strings.join(' &#187; ').html_safe)
534 end
536 end
535
537
536 # Sets the html title
538 # Sets the html title
537 # Returns the html title when called without arguments
539 # Returns the html title when called without arguments
538 # Current project name and app_title and automatically appended
540 # Current project name and app_title and automatically appended
539 # Exemples:
541 # Exemples:
540 # html_title 'Foo', 'Bar'
542 # html_title 'Foo', 'Bar'
541 # html_title # => 'Foo - Bar - My Project - Redmine'
543 # html_title # => 'Foo - Bar - My Project - Redmine'
542 def html_title(*args)
544 def html_title(*args)
543 if args.empty?
545 if args.empty?
544 title = @html_title || []
546 title = @html_title || []
545 title << @project.name if @project
547 title << @project.name if @project
546 title << Setting.app_title unless Setting.app_title == title.last
548 title << Setting.app_title unless Setting.app_title == title.last
547 title.reject(&:blank?).join(' - ')
549 title.reject(&:blank?).join(' - ')
548 else
550 else
549 @html_title ||= []
551 @html_title ||= []
550 @html_title += args
552 @html_title += args
551 end
553 end
552 end
554 end
553
555
554 # Returns the theme, controller name, and action as css classes for the
556 # Returns the theme, controller name, and action as css classes for the
555 # HTML body.
557 # HTML body.
556 def body_css_classes
558 def body_css_classes
557 css = []
559 css = []
558 if theme = Redmine::Themes.theme(Setting.ui_theme)
560 if theme = Redmine::Themes.theme(Setting.ui_theme)
559 css << 'theme-' + theme.name
561 css << 'theme-' + theme.name
560 end
562 end
561
563
562 css << 'project-' + @project.identifier if @project && @project.identifier.present?
564 css << 'project-' + @project.identifier if @project && @project.identifier.present?
563 css << 'controller-' + controller_name
565 css << 'controller-' + controller_name
564 css << 'action-' + action_name
566 css << 'action-' + action_name
565 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
567 if UserPreference::TEXTAREA_FONT_OPTIONS.include?(User.current.pref.textarea_font)
566 css << "textarea-#{User.current.pref.textarea_font}"
568 css << "textarea-#{User.current.pref.textarea_font}"
567 end
569 end
568 css.join(' ')
570 css.join(' ')
569 end
571 end
570
572
571 def accesskey(s)
573 def accesskey(s)
572 @used_accesskeys ||= []
574 @used_accesskeys ||= []
573 key = Redmine::AccessKeys.key_for(s)
575 key = Redmine::AccessKeys.key_for(s)
574 return nil if @used_accesskeys.include?(key)
576 return nil if @used_accesskeys.include?(key)
575 @used_accesskeys << key
577 @used_accesskeys << key
576 key
578 key
577 end
579 end
578
580
579 # Formats text according to system settings.
581 # Formats text according to system settings.
580 # 2 ways to call this method:
582 # 2 ways to call this method:
581 # * with a String: textilizable(text, options)
583 # * with a String: textilizable(text, options)
582 # * with an object and one of its attribute: textilizable(issue, :description, options)
584 # * with an object and one of its attribute: textilizable(issue, :description, options)
583 def textilizable(*args)
585 def textilizable(*args)
584 options = args.last.is_a?(Hash) ? args.pop : {}
586 options = args.last.is_a?(Hash) ? args.pop : {}
585 case args.size
587 case args.size
586 when 1
588 when 1
587 obj = options[:object]
589 obj = options[:object]
588 text = args.shift
590 text = args.shift
589 when 2
591 when 2
590 obj = args.shift
592 obj = args.shift
591 attr = args.shift
593 attr = args.shift
592 text = obj.send(attr).to_s
594 text = obj.send(attr).to_s
593 else
595 else
594 raise ArgumentError, 'invalid arguments to textilizable'
596 raise ArgumentError, 'invalid arguments to textilizable'
595 end
597 end
596 return '' if text.blank?
598 return '' if text.blank?
597 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
599 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
598 @only_path = only_path = options.delete(:only_path) == false ? false : true
600 @only_path = only_path = options.delete(:only_path) == false ? false : true
599
601
600 text = text.dup
602 text = text.dup
601 macros = catch_macros(text)
603 macros = catch_macros(text)
602 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
604 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
603
605
604 @parsed_headings = []
606 @parsed_headings = []
605 @heading_anchors = {}
607 @heading_anchors = {}
606 @current_section = 0 if options[:edit_section_links]
608 @current_section = 0 if options[:edit_section_links]
607
609
608 parse_sections(text, project, obj, attr, only_path, options)
610 parse_sections(text, project, obj, attr, only_path, options)
609 text = parse_non_pre_blocks(text, obj, macros) do |text|
611 text = parse_non_pre_blocks(text, obj, macros) do |text|
610 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
612 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
611 send method_name, text, project, obj, attr, only_path, options
613 send method_name, text, project, obj, attr, only_path, options
612 end
614 end
613 end
615 end
614 parse_headings(text, project, obj, attr, only_path, options)
616 parse_headings(text, project, obj, attr, only_path, options)
615
617
616 if @parsed_headings.any?
618 if @parsed_headings.any?
617 replace_toc(text, @parsed_headings)
619 replace_toc(text, @parsed_headings)
618 end
620 end
619
621
620 text.html_safe
622 text.html_safe
621 end
623 end
622
624
623 def parse_non_pre_blocks(text, obj, macros)
625 def parse_non_pre_blocks(text, obj, macros)
624 s = StringScanner.new(text)
626 s = StringScanner.new(text)
625 tags = []
627 tags = []
626 parsed = ''
628 parsed = ''
627 while !s.eos?
629 while !s.eos?
628 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
630 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
629 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
631 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
630 if tags.empty?
632 if tags.empty?
631 yield text
633 yield text
632 inject_macros(text, obj, macros) if macros.any?
634 inject_macros(text, obj, macros) if macros.any?
633 else
635 else
634 inject_macros(text, obj, macros, false) if macros.any?
636 inject_macros(text, obj, macros, false) if macros.any?
635 end
637 end
636 parsed << text
638 parsed << text
637 if tag
639 if tag
638 if closing
640 if closing
639 if tags.last && tags.last.casecmp(tag) == 0
641 if tags.last && tags.last.casecmp(tag) == 0
640 tags.pop
642 tags.pop
641 end
643 end
642 else
644 else
643 tags << tag.downcase
645 tags << tag.downcase
644 end
646 end
645 parsed << full_tag
647 parsed << full_tag
646 end
648 end
647 end
649 end
648 # Close any non closing tags
650 # Close any non closing tags
649 while tag = tags.pop
651 while tag = tags.pop
650 parsed << "</#{tag}>"
652 parsed << "</#{tag}>"
651 end
653 end
652 parsed
654 parsed
653 end
655 end
654
656
655 def parse_inline_attachments(text, project, obj, attr, only_path, options)
657 def parse_inline_attachments(text, project, obj, attr, only_path, options)
656 return if options[:inline_attachments] == false
658 return if options[:inline_attachments] == false
657
659
658 # when using an image link, try to use an attachment, if possible
660 # when using an image link, try to use an attachment, if possible
659 attachments = options[:attachments] || []
661 attachments = options[:attachments] || []
660 attachments += obj.attachments if obj.respond_to?(:attachments)
662 attachments += obj.attachments if obj.respond_to?(:attachments)
661 if attachments.present?
663 if attachments.present?
662 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
664 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
663 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
665 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
664 # search for the picture in attachments
666 # search for the picture in attachments
665 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
667 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
666 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
668 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
667 desc = found.description.to_s.gsub('"', '')
669 desc = found.description.to_s.gsub('"', '')
668 if !desc.blank? && alttext.blank?
670 if !desc.blank? && alttext.blank?
669 alt = " title=\"#{desc}\" alt=\"#{desc}\""
671 alt = " title=\"#{desc}\" alt=\"#{desc}\""
670 end
672 end
671 "src=\"#{image_url}\"#{alt}"
673 "src=\"#{image_url}\"#{alt}"
672 else
674 else
673 m
675 m
674 end
676 end
675 end
677 end
676 end
678 end
677 end
679 end
678
680
679 # Wiki links
681 # Wiki links
680 #
682 #
681 # Examples:
683 # Examples:
682 # [[mypage]]
684 # [[mypage]]
683 # [[mypage|mytext]]
685 # [[mypage|mytext]]
684 # wiki links can refer other project wikis, using project name or identifier:
686 # wiki links can refer other project wikis, using project name or identifier:
685 # [[project:]] -> wiki starting page
687 # [[project:]] -> wiki starting page
686 # [[project:|mytext]]
688 # [[project:|mytext]]
687 # [[project:mypage]]
689 # [[project:mypage]]
688 # [[project:mypage|mytext]]
690 # [[project:mypage|mytext]]
689 def parse_wiki_links(text, project, obj, attr, only_path, options)
691 def parse_wiki_links(text, project, obj, attr, only_path, options)
690 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
692 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
691 link_project = project
693 link_project = project
692 esc, all, page, title = $1, $2, $3, $5
694 esc, all, page, title = $1, $2, $3, $5
693 if esc.nil?
695 if esc.nil?
694 if page =~ /^([^\:]+)\:(.*)$/
696 if page =~ /^([^\:]+)\:(.*)$/
695 identifier, page = $1, $2
697 identifier, page = $1, $2
696 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
698 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
697 title ||= identifier if page.blank?
699 title ||= identifier if page.blank?
698 end
700 end
699
701
700 if link_project && link_project.wiki
702 if link_project && link_project.wiki
701 # extract anchor
703 # extract anchor
702 anchor = nil
704 anchor = nil
703 if page =~ /^(.+?)\#(.+)$/
705 if page =~ /^(.+?)\#(.+)$/
704 page, anchor = $1, $2
706 page, anchor = $1, $2
705 end
707 end
706 anchor = sanitize_anchor_name(anchor) if anchor.present?
708 anchor = sanitize_anchor_name(anchor) if anchor.present?
707 # check if page exists
709 # check if page exists
708 wiki_page = link_project.wiki.find_page(page)
710 wiki_page = link_project.wiki.find_page(page)
709 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
711 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
710 "##{anchor}"
712 "##{anchor}"
711 else
713 else
712 case options[:wiki_links]
714 case options[:wiki_links]
713 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
715 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
714 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
716 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
715 else
717 else
716 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
718 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
717 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
719 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
718 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
720 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
719 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
721 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
720 end
722 end
721 end
723 end
722 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
724 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
723 else
725 else
724 # project or wiki doesn't exist
726 # project or wiki doesn't exist
725 all
727 all
726 end
728 end
727 else
729 else
728 all
730 all
729 end
731 end
730 end
732 end
731 end
733 end
732
734
733 # Redmine links
735 # Redmine links
734 #
736 #
735 # Examples:
737 # Examples:
736 # Issues:
738 # Issues:
737 # #52 -> Link to issue #52
739 # #52 -> Link to issue #52
738 # Changesets:
740 # Changesets:
739 # r52 -> Link to revision 52
741 # r52 -> Link to revision 52
740 # commit:a85130f -> Link to scmid starting with a85130f
742 # commit:a85130f -> Link to scmid starting with a85130f
741 # Documents:
743 # Documents:
742 # document#17 -> Link to document with id 17
744 # document#17 -> Link to document with id 17
743 # document:Greetings -> Link to the document with title "Greetings"
745 # document:Greetings -> Link to the document with title "Greetings"
744 # document:"Some document" -> Link to the document with title "Some document"
746 # document:"Some document" -> Link to the document with title "Some document"
745 # Versions:
747 # Versions:
746 # version#3 -> Link to version with id 3
748 # version#3 -> Link to version with id 3
747 # version:1.0.0 -> Link to version named "1.0.0"
749 # version:1.0.0 -> Link to version named "1.0.0"
748 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
750 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
749 # Attachments:
751 # Attachments:
750 # attachment:file.zip -> Link to the attachment of the current object named file.zip
752 # attachment:file.zip -> Link to the attachment of the current object named file.zip
751 # Source files:
753 # Source files:
752 # source:some/file -> Link to the file located at /some/file in the project's repository
754 # source:some/file -> Link to the file located at /some/file in the project's repository
753 # source:some/file@52 -> Link to the file's revision 52
755 # source:some/file@52 -> Link to the file's revision 52
754 # source:some/file#L120 -> Link to line 120 of the file
756 # source:some/file#L120 -> Link to line 120 of the file
755 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
757 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
756 # export:some/file -> Force the download of the file
758 # export:some/file -> Force the download of the file
757 # Forum messages:
759 # Forum messages:
758 # message#1218 -> Link to message with id 1218
760 # message#1218 -> Link to message with id 1218
759 # Projects:
761 # Projects:
760 # project:someproject -> Link to project named "someproject"
762 # project:someproject -> Link to project named "someproject"
761 # project#3 -> Link to project with id 3
763 # project#3 -> Link to project with id 3
762 #
764 #
763 # Links can refer other objects from other projects, using project identifier:
765 # Links can refer other objects from other projects, using project identifier:
764 # identifier:r52
766 # identifier:r52
765 # identifier:document:"Some document"
767 # identifier:document:"Some document"
766 # identifier:version:1.0.0
768 # identifier:version:1.0.0
767 # identifier:source:some/file
769 # identifier:source:some/file
768 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
770 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
769 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|
771 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|
770 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
772 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
771 if tag_content
773 if tag_content
772 $&
774 $&
773 else
775 else
774 link = nil
776 link = nil
775 project = default_project
777 project = default_project
776 if project_identifier
778 if project_identifier
777 project = Project.visible.find_by_identifier(project_identifier)
779 project = Project.visible.find_by_identifier(project_identifier)
778 end
780 end
779 if esc.nil?
781 if esc.nil?
780 if prefix.nil? && sep == 'r'
782 if prefix.nil? && sep == 'r'
781 if project
783 if project
782 repository = nil
784 repository = nil
783 if repo_identifier
785 if repo_identifier
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
787 else
786 repository = project.repository
788 repository = project.repository
787 end
789 end
788 # project.changesets.visible raises an SQL error because of a double join on repositories
790 # project.changesets.visible raises an SQL error because of a double join on repositories
789 if repository &&
791 if repository &&
790 (changeset = Changeset.visible.
792 (changeset = Changeset.visible.
791 find_by_repository_id_and_revision(repository.id, identifier))
793 find_by_repository_id_and_revision(repository.id, identifier))
792 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
794 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
793 {:only_path => only_path, :controller => 'repositories',
795 {:only_path => only_path, :controller => 'repositories',
794 :action => 'revision', :id => project,
796 :action => 'revision', :id => project,
795 :repository_id => repository.identifier_param,
797 :repository_id => repository.identifier_param,
796 :rev => changeset.revision},
798 :rev => changeset.revision},
797 :class => 'changeset',
799 :class => 'changeset',
798 :title => truncate_single_line_raw(changeset.comments, 100))
800 :title => truncate_single_line_raw(changeset.comments, 100))
799 end
801 end
800 end
802 end
801 elsif sep == '#'
803 elsif sep == '#'
802 oid = identifier.to_i
804 oid = identifier.to_i
803 case prefix
805 case prefix
804 when nil
806 when nil
805 if oid.to_s == identifier &&
807 if oid.to_s == identifier &&
806 issue = Issue.visible.find_by_id(oid)
808 issue = Issue.visible.find_by_id(oid)
807 anchor = comment_id ? "note-#{comment_id}" : nil
809 anchor = comment_id ? "note-#{comment_id}" : nil
808 link = link_to("##{oid}#{comment_suffix}",
810 link = link_to("##{oid}#{comment_suffix}",
809 issue_url(issue, :only_path => only_path, :anchor => anchor),
811 issue_url(issue, :only_path => only_path, :anchor => anchor),
810 :class => issue.css_classes,
812 :class => issue.css_classes,
811 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
813 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
812 end
814 end
813 when 'document'
815 when 'document'
814 if document = Document.visible.find_by_id(oid)
816 if document = Document.visible.find_by_id(oid)
815 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
817 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
816 end
818 end
817 when 'version'
819 when 'version'
818 if version = Version.visible.find_by_id(oid)
820 if version = Version.visible.find_by_id(oid)
819 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
821 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
820 end
822 end
821 when 'message'
823 when 'message'
822 if message = Message.visible.find_by_id(oid)
824 if message = Message.visible.find_by_id(oid)
823 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
825 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
824 end
826 end
825 when 'forum'
827 when 'forum'
826 if board = Board.visible.find_by_id(oid)
828 if board = Board.visible.find_by_id(oid)
827 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
829 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
828 end
830 end
829 when 'news'
831 when 'news'
830 if news = News.visible.find_by_id(oid)
832 if news = News.visible.find_by_id(oid)
831 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
833 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
832 end
834 end
833 when 'project'
835 when 'project'
834 if p = Project.visible.find_by_id(oid)
836 if p = Project.visible.find_by_id(oid)
835 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
837 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
836 end
838 end
837 end
839 end
838 elsif sep == ':'
840 elsif sep == ':'
839 # removes the double quotes if any
841 # removes the double quotes if any
840 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
842 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
841 name = CGI.unescapeHTML(name)
843 name = CGI.unescapeHTML(name)
842 case prefix
844 case prefix
843 when 'document'
845 when 'document'
844 if project && document = project.documents.visible.find_by_title(name)
846 if project && document = project.documents.visible.find_by_title(name)
845 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
847 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
846 end
848 end
847 when 'version'
849 when 'version'
848 if project && version = project.versions.visible.find_by_name(name)
850 if project && version = project.versions.visible.find_by_name(name)
849 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
851 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
850 end
852 end
851 when 'forum'
853 when 'forum'
852 if project && board = project.boards.visible.find_by_name(name)
854 if project && board = project.boards.visible.find_by_name(name)
853 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
855 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
854 end
856 end
855 when 'news'
857 when 'news'
856 if project && news = project.news.visible.find_by_title(name)
858 if project && news = project.news.visible.find_by_title(name)
857 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
859 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
858 end
860 end
859 when 'commit', 'source', 'export'
861 when 'commit', 'source', 'export'
860 if project
862 if project
861 repository = nil
863 repository = nil
862 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
864 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
863 repo_prefix, repo_identifier, name = $1, $2, $3
865 repo_prefix, repo_identifier, name = $1, $2, $3
864 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
866 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
865 else
867 else
866 repository = project.repository
868 repository = project.repository
867 end
869 end
868 if prefix == 'commit'
870 if prefix == 'commit'
869 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
871 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
870 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},
872 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},
871 :class => 'changeset',
873 :class => 'changeset',
872 :title => truncate_single_line_raw(changeset.comments, 100)
874 :title => truncate_single_line_raw(changeset.comments, 100)
873 end
875 end
874 else
876 else
875 if repository && User.current.allowed_to?(:browse_repository, project)
877 if repository && User.current.allowed_to?(:browse_repository, project)
876 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
878 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
877 path, rev, anchor = $1, $3, $5
879 path, rev, anchor = $1, $3, $5
878 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,
880 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,
879 :path => to_path_param(path),
881 :path => to_path_param(path),
880 :rev => rev,
882 :rev => rev,
881 :anchor => anchor},
883 :anchor => anchor},
882 :class => (prefix == 'export' ? 'source download' : 'source')
884 :class => (prefix == 'export' ? 'source download' : 'source')
883 end
885 end
884 end
886 end
885 repo_prefix = nil
887 repo_prefix = nil
886 end
888 end
887 when 'attachment'
889 when 'attachment'
888 attachments = options[:attachments] || []
890 attachments = options[:attachments] || []
889 attachments += obj.attachments if obj.respond_to?(:attachments)
891 attachments += obj.attachments if obj.respond_to?(:attachments)
890 if attachments && attachment = Attachment.latest_attach(attachments, name)
892 if attachments && attachment = Attachment.latest_attach(attachments, name)
891 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
893 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
892 end
894 end
893 when 'project'
895 when 'project'
894 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
896 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
895 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
897 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
896 end
898 end
897 end
899 end
898 end
900 end
899 end
901 end
900 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
902 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
901 end
903 end
902 end
904 end
903 end
905 end
904
906
905 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
907 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
906
908
907 def parse_sections(text, project, obj, attr, only_path, options)
909 def parse_sections(text, project, obj, attr, only_path, options)
908 return unless options[:edit_section_links]
910 return unless options[:edit_section_links]
909 text.gsub!(HEADING_RE) do
911 text.gsub!(HEADING_RE) do
910 heading, level = $1, $2
912 heading, level = $1, $2
911 @current_section += 1
913 @current_section += 1
912 if @current_section > 1
914 if @current_section > 1
913 content_tag('div',
915 content_tag('div',
914 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
916 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
915 :class => 'icon-only icon-edit'),
917 :class => 'icon-only icon-edit'),
916 :class => "contextual heading-#{level}",
918 :class => "contextual heading-#{level}",
917 :title => l(:button_edit_section),
919 :title => l(:button_edit_section),
918 :id => "section-#{@current_section}") + heading.html_safe
920 :id => "section-#{@current_section}") + heading.html_safe
919 else
921 else
920 heading
922 heading
921 end
923 end
922 end
924 end
923 end
925 end
924
926
925 # Headings and TOC
927 # Headings and TOC
926 # Adds ids and links to headings unless options[:headings] is set to false
928 # Adds ids and links to headings unless options[:headings] is set to false
927 def parse_headings(text, project, obj, attr, only_path, options)
929 def parse_headings(text, project, obj, attr, only_path, options)
928 return if options[:headings] == false
930 return if options[:headings] == false
929
931
930 text.gsub!(HEADING_RE) do
932 text.gsub!(HEADING_RE) do
931 level, attrs, content = $2.to_i, $3, $4
933 level, attrs, content = $2.to_i, $3, $4
932 item = strip_tags(content).strip
934 item = strip_tags(content).strip
933 anchor = sanitize_anchor_name(item)
935 anchor = sanitize_anchor_name(item)
934 # used for single-file wiki export
936 # used for single-file wiki export
935 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
937 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
936 @heading_anchors[anchor] ||= 0
938 @heading_anchors[anchor] ||= 0
937 idx = (@heading_anchors[anchor] += 1)
939 idx = (@heading_anchors[anchor] += 1)
938 if idx > 1
940 if idx > 1
939 anchor = "#{anchor}-#{idx}"
941 anchor = "#{anchor}-#{idx}"
940 end
942 end
941 @parsed_headings << [level, anchor, item]
943 @parsed_headings << [level, anchor, item]
942 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
944 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
943 end
945 end
944 end
946 end
945
947
946 MACROS_RE = /(
948 MACROS_RE = /(
947 (!)? # escaping
949 (!)? # escaping
948 (
950 (
949 \{\{ # opening tag
951 \{\{ # opening tag
950 ([\w]+) # macro name
952 ([\w]+) # macro name
951 (\(([^\n\r]*?)\))? # optional arguments
953 (\(([^\n\r]*?)\))? # optional arguments
952 ([\n\r].*?[\n\r])? # optional block of text
954 ([\n\r].*?[\n\r])? # optional block of text
953 \}\} # closing tag
955 \}\} # closing tag
954 )
956 )
955 )/mx unless const_defined?(:MACROS_RE)
957 )/mx unless const_defined?(:MACROS_RE)
956
958
957 MACRO_SUB_RE = /(
959 MACRO_SUB_RE = /(
958 \{\{
960 \{\{
959 macro\((\d+)\)
961 macro\((\d+)\)
960 \}\}
962 \}\}
961 )/x unless const_defined?(:MACRO_SUB_RE)
963 )/x unless const_defined?(:MACRO_SUB_RE)
962
964
963 # Extracts macros from text
965 # Extracts macros from text
964 def catch_macros(text)
966 def catch_macros(text)
965 macros = {}
967 macros = {}
966 text.gsub!(MACROS_RE) do
968 text.gsub!(MACROS_RE) do
967 all, macro = $1, $4.downcase
969 all, macro = $1, $4.downcase
968 if macro_exists?(macro) || all =~ MACRO_SUB_RE
970 if macro_exists?(macro) || all =~ MACRO_SUB_RE
969 index = macros.size
971 index = macros.size
970 macros[index] = all
972 macros[index] = all
971 "{{macro(#{index})}}"
973 "{{macro(#{index})}}"
972 else
974 else
973 all
975 all
974 end
976 end
975 end
977 end
976 macros
978 macros
977 end
979 end
978
980
979 # Executes and replaces macros in text
981 # Executes and replaces macros in text
980 def inject_macros(text, obj, macros, execute=true)
982 def inject_macros(text, obj, macros, execute=true)
981 text.gsub!(MACRO_SUB_RE) do
983 text.gsub!(MACRO_SUB_RE) do
982 all, index = $1, $2.to_i
984 all, index = $1, $2.to_i
983 orig = macros.delete(index)
985 orig = macros.delete(index)
984 if execute && orig && orig =~ MACROS_RE
986 if execute && orig && orig =~ MACROS_RE
985 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
987 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
986 if esc.nil?
988 if esc.nil?
987 h(exec_macro(macro, obj, args, block) || all)
989 h(exec_macro(macro, obj, args, block) || all)
988 else
990 else
989 h(all)
991 h(all)
990 end
992 end
991 elsif orig
993 elsif orig
992 h(orig)
994 h(orig)
993 else
995 else
994 h(all)
996 h(all)
995 end
997 end
996 end
998 end
997 end
999 end
998
1000
999 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1001 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
1000
1002
1001 # Renders the TOC with given headings
1003 # Renders the TOC with given headings
1002 def replace_toc(text, headings)
1004 def replace_toc(text, headings)
1003 text.gsub!(TOC_RE) do
1005 text.gsub!(TOC_RE) do
1004 left_align, right_align = $2, $3
1006 left_align, right_align = $2, $3
1005 # Keep only the 4 first levels
1007 # Keep only the 4 first levels
1006 headings = headings.select{|level, anchor, item| level <= 4}
1008 headings = headings.select{|level, anchor, item| level <= 4}
1007 if headings.empty?
1009 if headings.empty?
1008 ''
1010 ''
1009 else
1011 else
1010 div_class = 'toc'
1012 div_class = 'toc'
1011 div_class << ' right' if right_align
1013 div_class << ' right' if right_align
1012 div_class << ' left' if left_align
1014 div_class << ' left' if left_align
1013 out = "<ul class=\"#{div_class}\"><li>"
1015 out = "<ul class=\"#{div_class}\"><li>"
1014 root = headings.map(&:first).min
1016 root = headings.map(&:first).min
1015 current = root
1017 current = root
1016 started = false
1018 started = false
1017 headings.each do |level, anchor, item|
1019 headings.each do |level, anchor, item|
1018 if level > current
1020 if level > current
1019 out << '<ul><li>' * (level - current)
1021 out << '<ul><li>' * (level - current)
1020 elsif level < current
1022 elsif level < current
1021 out << "</li></ul>\n" * (current - level) + "</li><li>"
1023 out << "</li></ul>\n" * (current - level) + "</li><li>"
1022 elsif started
1024 elsif started
1023 out << '</li><li>'
1025 out << '</li><li>'
1024 end
1026 end
1025 out << "<a href=\"##{anchor}\">#{item}</a>"
1027 out << "<a href=\"##{anchor}\">#{item}</a>"
1026 current = level
1028 current = level
1027 started = true
1029 started = true
1028 end
1030 end
1029 out << '</li></ul>' * (current - root)
1031 out << '</li></ul>' * (current - root)
1030 out << '</li></ul>'
1032 out << '</li></ul>'
1031 end
1033 end
1032 end
1034 end
1033 end
1035 end
1034
1036
1035 # Same as Rails' simple_format helper without using paragraphs
1037 # Same as Rails' simple_format helper without using paragraphs
1036 def simple_format_without_paragraph(text)
1038 def simple_format_without_paragraph(text)
1037 text.to_s.
1039 text.to_s.
1038 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1040 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1039 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1041 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1040 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1042 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1041 html_safe
1043 html_safe
1042 end
1044 end
1043
1045
1044 def lang_options_for_select(blank=true)
1046 def lang_options_for_select(blank=true)
1045 (blank ? [["(auto)", ""]] : []) + languages_options
1047 (blank ? [["(auto)", ""]] : []) + languages_options
1046 end
1048 end
1047
1049
1048 def labelled_form_for(*args, &proc)
1050 def labelled_form_for(*args, &proc)
1049 args << {} unless args.last.is_a?(Hash)
1051 args << {} unless args.last.is_a?(Hash)
1050 options = args.last
1052 options = args.last
1051 if args.first.is_a?(Symbol)
1053 if args.first.is_a?(Symbol)
1052 options.merge!(:as => args.shift)
1054 options.merge!(:as => args.shift)
1053 end
1055 end
1054 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1055 form_for(*args, &proc)
1057 form_for(*args, &proc)
1056 end
1058 end
1057
1059
1058 def labelled_fields_for(*args, &proc)
1060 def labelled_fields_for(*args, &proc)
1059 args << {} unless args.last.is_a?(Hash)
1061 args << {} unless args.last.is_a?(Hash)
1060 options = args.last
1062 options = args.last
1061 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1063 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1062 fields_for(*args, &proc)
1064 fields_for(*args, &proc)
1063 end
1065 end
1064
1066
1065 # Render the error messages for the given objects
1067 # Render the error messages for the given objects
1066 def error_messages_for(*objects)
1068 def error_messages_for(*objects)
1067 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1069 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1068 errors = objects.map {|o| o.errors.full_messages}.flatten
1070 errors = objects.map {|o| o.errors.full_messages}.flatten
1069 render_error_messages(errors)
1071 render_error_messages(errors)
1070 end
1072 end
1071
1073
1072 # Renders a list of error messages
1074 # Renders a list of error messages
1073 def render_error_messages(errors)
1075 def render_error_messages(errors)
1074 html = ""
1076 html = ""
1075 if errors.present?
1077 if errors.present?
1076 html << "<div id='errorExplanation'><ul>\n"
1078 html << "<div id='errorExplanation'><ul>\n"
1077 errors.each do |error|
1079 errors.each do |error|
1078 html << "<li>#{h error}</li>\n"
1080 html << "<li>#{h error}</li>\n"
1079 end
1081 end
1080 html << "</ul></div>\n"
1082 html << "</ul></div>\n"
1081 end
1083 end
1082 html.html_safe
1084 html.html_safe
1083 end
1085 end
1084
1086
1085 def delete_link(url, options={})
1087 def delete_link(url, options={})
1086 options = {
1088 options = {
1087 :method => :delete,
1089 :method => :delete,
1088 :data => {:confirm => l(:text_are_you_sure)},
1090 :data => {:confirm => l(:text_are_you_sure)},
1089 :class => 'icon icon-del'
1091 :class => 'icon icon-del'
1090 }.merge(options)
1092 }.merge(options)
1091
1093
1092 link_to l(:button_delete), url, options
1094 link_to l(:button_delete), url, options
1093 end
1095 end
1094
1096
1095 def preview_link(url, form, target='preview', options={})
1097 def preview_link(url, form, target='preview', options={})
1096 content_tag 'a', l(:label_preview), {
1098 content_tag 'a', l(:label_preview), {
1097 :href => "#",
1099 :href => "#",
1098 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1100 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1099 :accesskey => accesskey(:preview)
1101 :accesskey => accesskey(:preview)
1100 }.merge(options)
1102 }.merge(options)
1101 end
1103 end
1102
1104
1103 def link_to_function(name, function, html_options={})
1105 def link_to_function(name, function, html_options={})
1104 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1106 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1105 end
1107 end
1106
1108
1107 # Helper to render JSON in views
1109 # Helper to render JSON in views
1108 def raw_json(arg)
1110 def raw_json(arg)
1109 arg.to_json.to_s.gsub('/', '\/').html_safe
1111 arg.to_json.to_s.gsub('/', '\/').html_safe
1110 end
1112 end
1111
1113
1112 def back_url
1114 def back_url
1113 url = params[:back_url]
1115 url = params[:back_url]
1114 if url.nil? && referer = request.env['HTTP_REFERER']
1116 if url.nil? && referer = request.env['HTTP_REFERER']
1115 url = CGI.unescape(referer.to_s)
1117 url = CGI.unescape(referer.to_s)
1116 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1118 # URLs that contains the utf8=[checkmark] parameter added by Rails are
1117 # parsed as invalid by URI.parse so the redirect to the back URL would
1119 # parsed as invalid by URI.parse so the redirect to the back URL would
1118 # not be accepted (ApplicationController#validate_back_url would return
1120 # not be accepted (ApplicationController#validate_back_url would return
1119 # false)
1121 # false)
1120 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1122 url.gsub!(/(\?|&)utf8=\u2713&?/, '\1')
1121 end
1123 end
1122 url
1124 url
1123 end
1125 end
1124
1126
1125 def back_url_hidden_field_tag
1127 def back_url_hidden_field_tag
1126 url = back_url
1128 url = back_url
1127 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1129 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1128 end
1130 end
1129
1131
1130 def check_all_links(form_name)
1132 def check_all_links(form_name)
1131 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1133 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1132 " | ".html_safe +
1134 " | ".html_safe +
1133 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1135 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1134 end
1136 end
1135
1137
1136 def toggle_checkboxes_link(selector)
1138 def toggle_checkboxes_link(selector)
1137 link_to_function '',
1139 link_to_function '',
1138 "toggleCheckboxesBySelector('#{selector}')",
1140 "toggleCheckboxesBySelector('#{selector}')",
1139 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1141 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1140 :class => 'toggle-checkboxes'
1142 :class => 'toggle-checkboxes'
1141 end
1143 end
1142
1144
1143 def progress_bar(pcts, options={})
1145 def progress_bar(pcts, options={})
1144 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1146 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1145 pcts = pcts.collect(&:round)
1147 pcts = pcts.collect(&:round)
1146 pcts[1] = pcts[1] - pcts[0]
1148 pcts[1] = pcts[1] - pcts[0]
1147 pcts << (100 - pcts[1] - pcts[0])
1149 pcts << (100 - pcts[1] - pcts[0])
1148 titles = options[:titles].to_a
1150 titles = options[:titles].to_a
1149 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1151 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1150 legend = options[:legend] || ''
1152 legend = options[:legend] || ''
1151 content_tag('table',
1153 content_tag('table',
1152 content_tag('tr',
1154 content_tag('tr',
1153 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1155 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1154 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1156 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1155 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1157 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1156 ), :class => "progress progress-#{pcts[0]}").html_safe +
1158 ), :class => "progress progress-#{pcts[0]}").html_safe +
1157 content_tag('p', legend, :class => 'percent').html_safe
1159 content_tag('p', legend, :class => 'percent').html_safe
1158 end
1160 end
1159
1161
1160 def checked_image(checked=true)
1162 def checked_image(checked=true)
1161 if checked
1163 if checked
1162 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1164 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1163 end
1165 end
1164 end
1166 end
1165
1167
1166 def context_menu(url)
1168 def context_menu(url)
1167 unless @context_menu_included
1169 unless @context_menu_included
1168 content_for :header_tags do
1170 content_for :header_tags do
1169 javascript_include_tag('context_menu') +
1171 javascript_include_tag('context_menu') +
1170 stylesheet_link_tag('context_menu')
1172 stylesheet_link_tag('context_menu')
1171 end
1173 end
1172 if l(:direction) == 'rtl'
1174 if l(:direction) == 'rtl'
1173 content_for :header_tags do
1175 content_for :header_tags do
1174 stylesheet_link_tag('context_menu_rtl')
1176 stylesheet_link_tag('context_menu_rtl')
1175 end
1177 end
1176 end
1178 end
1177 @context_menu_included = true
1179 @context_menu_included = true
1178 end
1180 end
1179 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1181 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1180 end
1182 end
1181
1183
1182 def calendar_for(field_id)
1184 def calendar_for(field_id)
1183 include_calendar_headers_tags
1185 include_calendar_headers_tags
1184 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1186 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1185 end
1187 end
1186
1188
1187 def include_calendar_headers_tags
1189 def include_calendar_headers_tags
1188 unless @calendar_headers_tags_included
1190 unless @calendar_headers_tags_included
1189 tags = ''.html_safe
1191 tags = ''.html_safe
1190 @calendar_headers_tags_included = true
1192 @calendar_headers_tags_included = true
1191 content_for :header_tags do
1193 content_for :header_tags do
1192 start_of_week = Setting.start_of_week
1194 start_of_week = Setting.start_of_week
1193 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1195 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1194 # Redmine uses 1..7 (monday..sunday) in settings and locales
1196 # Redmine uses 1..7 (monday..sunday) in settings and locales
1195 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1197 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1196 start_of_week = start_of_week.to_i % 7
1198 start_of_week = start_of_week.to_i % 7
1197 tags << javascript_tag(
1199 tags << javascript_tag(
1198 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1200 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1199 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1201 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1200 path_to_image('/images/calendar.png') +
1202 path_to_image('/images/calendar.png') +
1201 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1203 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1202 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1204 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1203 "beforeShow: beforeShowDatePicker};")
1205 "beforeShow: beforeShowDatePicker};")
1204 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1206 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1205 unless jquery_locale == 'en'
1207 unless jquery_locale == 'en'
1206 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1208 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1207 end
1209 end
1208 tags
1210 tags
1209 end
1211 end
1210 end
1212 end
1211 end
1213 end
1212
1214
1213 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1215 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1214 # Examples:
1216 # Examples:
1215 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1217 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1216 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1218 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1217 #
1219 #
1218 def stylesheet_link_tag(*sources)
1220 def stylesheet_link_tag(*sources)
1219 options = sources.last.is_a?(Hash) ? sources.pop : {}
1221 options = sources.last.is_a?(Hash) ? sources.pop : {}
1220 plugin = options.delete(:plugin)
1222 plugin = options.delete(:plugin)
1221 sources = sources.map do |source|
1223 sources = sources.map do |source|
1222 if plugin
1224 if plugin
1223 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1225 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1224 elsif current_theme && current_theme.stylesheets.include?(source)
1226 elsif current_theme && current_theme.stylesheets.include?(source)
1225 current_theme.stylesheet_path(source)
1227 current_theme.stylesheet_path(source)
1226 else
1228 else
1227 source
1229 source
1228 end
1230 end
1229 end
1231 end
1230 super *sources, options
1232 super *sources, options
1231 end
1233 end
1232
1234
1233 # Overrides Rails' image_tag with themes and plugins support.
1235 # Overrides Rails' image_tag with themes and plugins support.
1234 # Examples:
1236 # Examples:
1235 # image_tag('image.png') # => picks image.png from the current theme or defaults
1237 # image_tag('image.png') # => picks image.png from the current theme or defaults
1236 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1238 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1237 #
1239 #
1238 def image_tag(source, options={})
1240 def image_tag(source, options={})
1239 if plugin = options.delete(:plugin)
1241 if plugin = options.delete(:plugin)
1240 source = "/plugin_assets/#{plugin}/images/#{source}"
1242 source = "/plugin_assets/#{plugin}/images/#{source}"
1241 elsif current_theme && current_theme.images.include?(source)
1243 elsif current_theme && current_theme.images.include?(source)
1242 source = current_theme.image_path(source)
1244 source = current_theme.image_path(source)
1243 end
1245 end
1244 super source, options
1246 super source, options
1245 end
1247 end
1246
1248
1247 # Overrides Rails' javascript_include_tag with plugins support
1249 # Overrides Rails' javascript_include_tag with plugins support
1248 # Examples:
1250 # Examples:
1249 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1251 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1250 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1252 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1251 #
1253 #
1252 def javascript_include_tag(*sources)
1254 def javascript_include_tag(*sources)
1253 options = sources.last.is_a?(Hash) ? sources.pop : {}
1255 options = sources.last.is_a?(Hash) ? sources.pop : {}
1254 if plugin = options.delete(:plugin)
1256 if plugin = options.delete(:plugin)
1255 sources = sources.map do |source|
1257 sources = sources.map do |source|
1256 if plugin
1258 if plugin
1257 "/plugin_assets/#{plugin}/javascripts/#{source}"
1259 "/plugin_assets/#{plugin}/javascripts/#{source}"
1258 else
1260 else
1259 source
1261 source
1260 end
1262 end
1261 end
1263 end
1262 end
1264 end
1263 super *sources, options
1265 super *sources, options
1264 end
1266 end
1265
1267
1266 def sidebar_content?
1268 def sidebar_content?
1267 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1269 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1268 end
1270 end
1269
1271
1270 def view_layouts_base_sidebar_hook_response
1272 def view_layouts_base_sidebar_hook_response
1271 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1273 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1272 end
1274 end
1273
1275
1274 def email_delivery_enabled?
1276 def email_delivery_enabled?
1275 !!ActionMailer::Base.perform_deliveries
1277 !!ActionMailer::Base.perform_deliveries
1276 end
1278 end
1277
1279
1278 # Returns the avatar image tag for the given +user+ if avatars are enabled
1280 # Returns the avatar image tag for the given +user+ if avatars are enabled
1279 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1281 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1280 def avatar(user, options = { })
1282 def avatar(user, options = { })
1281 if Setting.gravatar_enabled?
1283 if Setting.gravatar_enabled?
1282 options.merge!(:default => Setting.gravatar_default)
1284 options.merge!(:default => Setting.gravatar_default)
1283 email = nil
1285 email = nil
1284 if user.respond_to?(:mail)
1286 if user.respond_to?(:mail)
1285 email = user.mail
1287 email = user.mail
1286 elsif user.to_s =~ %r{<(.+?)>}
1288 elsif user.to_s =~ %r{<(.+?)>}
1287 email = $1
1289 email = $1
1288 end
1290 end
1289 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1291 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1290 else
1292 else
1291 ''
1293 ''
1292 end
1294 end
1293 end
1295 end
1294
1296
1295 # Returns a link to edit user's avatar if avatars are enabled
1297 # Returns a link to edit user's avatar if avatars are enabled
1296 def avatar_edit_link(user, options={})
1298 def avatar_edit_link(user, options={})
1297 if Setting.gravatar_enabled?
1299 if Setting.gravatar_enabled?
1298 url = "https://gravatar.com"
1300 url = "https://gravatar.com"
1299 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1301 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1300 end
1302 end
1301 end
1303 end
1302
1304
1303 def sanitize_anchor_name(anchor)
1305 def sanitize_anchor_name(anchor)
1304 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1306 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1305 end
1307 end
1306
1308
1307 # Returns the javascript tags that are included in the html layout head
1309 # Returns the javascript tags that are included in the html layout head
1308 def javascript_heads
1310 def javascript_heads
1309 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1311 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1310 unless User.current.pref.warn_on_leaving_unsaved == '0'
1312 unless User.current.pref.warn_on_leaving_unsaved == '0'
1311 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1313 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1312 end
1314 end
1313 tags
1315 tags
1314 end
1316 end
1315
1317
1316 def favicon
1318 def favicon
1317 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1319 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1318 end
1320 end
1319
1321
1320 # Returns the path to the favicon
1322 # Returns the path to the favicon
1321 def favicon_path
1323 def favicon_path
1322 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1324 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1323 image_path(icon)
1325 image_path(icon)
1324 end
1326 end
1325
1327
1326 # Returns the full URL to the favicon
1328 # Returns the full URL to the favicon
1327 def favicon_url
1329 def favicon_url
1328 # TODO: use #image_url introduced in Rails4
1330 # TODO: use #image_url introduced in Rails4
1329 path = favicon_path
1331 path = favicon_path
1330 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1332 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1331 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1333 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1332 end
1334 end
1333
1335
1334 def robot_exclusion_tag
1336 def robot_exclusion_tag
1335 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1337 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1336 end
1338 end
1337
1339
1338 # Returns true if arg is expected in the API response
1340 # Returns true if arg is expected in the API response
1339 def include_in_api_response?(arg)
1341 def include_in_api_response?(arg)
1340 unless @included_in_api_response
1342 unless @included_in_api_response
1341 param = params[:include]
1343 param = params[:include]
1342 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1344 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1343 @included_in_api_response.collect!(&:strip)
1345 @included_in_api_response.collect!(&:strip)
1344 end
1346 end
1345 @included_in_api_response.include?(arg.to_s)
1347 @included_in_api_response.include?(arg.to_s)
1346 end
1348 end
1347
1349
1348 # Returns options or nil if nometa param or X-Redmine-Nometa header
1350 # Returns options or nil if nometa param or X-Redmine-Nometa header
1349 # was set in the request
1351 # was set in the request
1350 def api_meta(options)
1352 def api_meta(options)
1351 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1353 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1352 # compatibility mode for activeresource clients that raise
1354 # compatibility mode for activeresource clients that raise
1353 # an error when deserializing an array with attributes
1355 # an error when deserializing an array with attributes
1354 nil
1356 nil
1355 else
1357 else
1356 options
1358 options
1357 end
1359 end
1358 end
1360 end
1359
1361
1360 def generate_csv(&block)
1362 def generate_csv(&block)
1361 decimal_separator = l(:general_csv_decimal_separator)
1363 decimal_separator = l(:general_csv_decimal_separator)
1362 encoding = l(:general_csv_encoding)
1364 encoding = l(:general_csv_encoding)
1363 end
1365 end
1364
1366
1365 private
1367 private
1366
1368
1367 def wiki_helper
1369 def wiki_helper
1368 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1370 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1369 extend helper
1371 extend helper
1370 return self
1372 return self
1371 end
1373 end
1372 end
1374 end
@@ -1,479 +1,484
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 module IssuesHelper
20 module IssuesHelper
21 include ApplicationHelper
21 include ApplicationHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
22 include Redmine::Export::PDF::IssuesPdfHelper
23
23
24 def issue_list(issues, &block)
24 def issue_list(issues, &block)
25 ancestors = []
25 ancestors = []
26 issues.each do |issue|
26 issues.each do |issue|
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
27 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
28 ancestors.pop
28 ancestors.pop
29 end
29 end
30 yield issue, ancestors.size
30 yield issue, ancestors.size
31 ancestors << issue unless issue.leaf?
31 ancestors << issue unless issue.leaf?
32 end
32 end
33 end
33 end
34
34
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
35 def grouped_issue_list(issues, query, issue_count_by_group, &block)
36 ancestors = []
36 ancestors = []
37 grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals|
37 grouped_query_results(issues, query, issue_count_by_group) do |issue, group_name, group_count, group_totals|
38 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
38 while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
39 ancestors.pop
39 ancestors.pop
40 end
40 end
41 yield issue, ancestors.size, group_name, group_count, group_totals
41 yield issue, ancestors.size, group_name, group_count, group_totals
42 ancestors << issue unless issue.leaf?
42 ancestors << issue unless issue.leaf?
43 end
43 end
44 end
44 end
45
45
46 # Renders a HTML/CSS tooltip
46 # Renders a HTML/CSS tooltip
47 #
47 #
48 # To use, a trigger div is needed. This is a div with the class of "tooltip"
48 # To use, a trigger div is needed. This is a div with the class of "tooltip"
49 # that contains this method wrapped in a span with the class of "tip"
49 # that contains this method wrapped in a span with the class of "tip"
50 #
50 #
51 # <div class="tooltip"><%= link_to_issue(issue) %>
51 # <div class="tooltip"><%= link_to_issue(issue) %>
52 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
52 # <span class="tip"><%= render_issue_tooltip(issue) %></span>
53 # </div>
53 # </div>
54 #
54 #
55 def render_issue_tooltip(issue)
55 def render_issue_tooltip(issue)
56 @cached_label_status ||= l(:field_status)
56 @cached_label_status ||= l(:field_status)
57 @cached_label_start_date ||= l(:field_start_date)
57 @cached_label_start_date ||= l(:field_start_date)
58 @cached_label_due_date ||= l(:field_due_date)
58 @cached_label_due_date ||= l(:field_due_date)
59 @cached_label_assigned_to ||= l(:field_assigned_to)
59 @cached_label_assigned_to ||= l(:field_assigned_to)
60 @cached_label_priority ||= l(:field_priority)
60 @cached_label_priority ||= l(:field_priority)
61 @cached_label_project ||= l(:field_project)
61 @cached_label_project ||= l(:field_project)
62
62
63 link_to_issue(issue) + "<br /><br />".html_safe +
63 link_to_issue(issue) + "<br /><br />".html_safe +
64 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
64 "<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
65 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
65 "<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
66 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
66 "<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
67 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
67 "<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
68 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
68 "<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
69 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
69 "<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
70 end
70 end
71
71
72 def issue_heading(issue)
72 def issue_heading(issue)
73 h("#{issue.tracker} ##{issue.id}")
73 h("#{issue.tracker} ##{issue.id}")
74 end
74 end
75
75
76 def render_issue_subject_with_tree(issue)
76 def render_issue_subject_with_tree(issue)
77 s = ''
77 s = ''
78 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
78 ancestors = issue.root? ? [] : issue.ancestors.visible.to_a
79 ancestors.each do |ancestor|
79 ancestors.each do |ancestor|
80 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
80 s << '<div>' + content_tag('p', link_to_issue(ancestor, :project => (issue.project_id != ancestor.project_id)))
81 end
81 end
82 s << '<div>'
82 s << '<div>'
83 subject = h(issue.subject)
83 subject = h(issue.subject)
84 if issue.is_private?
84 if issue.is_private?
85 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
85 subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
86 end
86 end
87 s << content_tag('h3', subject)
87 s << content_tag('h3', subject)
88 s << '</div>' * (ancestors.size + 1)
88 s << '</div>' * (ancestors.size + 1)
89 s.html_safe
89 s.html_safe
90 end
90 end
91
91
92 def render_descendants_tree(issue)
92 def render_descendants_tree(issue)
93 s = '<form><table class="list issues">'
93 s = '<form><table class="list issues">'
94 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
94 issue_list(issue.descendants.visible.preload(:status, :priority, :tracker, :assigned_to).sort_by(&:lft)) do |child, level|
95 css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
95 css = "issue issue-#{child.id} hascontextmenu #{child.css_classes}"
96 css << " idnt idnt-#{level}" if level > 0
96 css << " idnt idnt-#{level}" if level > 0
97 s << content_tag('tr',
97 s << content_tag('tr',
98 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
98 content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
99 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
99 content_tag('td', link_to_issue(child, :project => (issue.project_id != child.project_id)), :class => 'subject', :style => 'width: 50%') +
100 content_tag('td', h(child.status), :class => 'status') +
100 content_tag('td', h(child.status), :class => 'status') +
101 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
101 content_tag('td', link_to_user(child.assigned_to), :class => 'assigned_to') +
102 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
102 content_tag('td', child.disabled_core_fields.include?('done_ratio') ? '' : progress_bar(child.done_ratio), :class=> 'done_ratio'),
103 :class => css)
103 :class => css)
104 end
104 end
105 s << '</table></form>'
105 s << '</table></form>'
106 s.html_safe
106 s.html_safe
107 end
107 end
108
108
109 def issue_estimated_hours_details(issue)
109 def issue_estimated_hours_details(issue)
110 if issue.total_estimated_hours.present?
110 if issue.total_estimated_hours.present?
111 if issue.total_estimated_hours == issue.estimated_hours
111 if issue.total_estimated_hours == issue.estimated_hours
112 l_hours_short(issue.estimated_hours)
112 l_hours_short(issue.estimated_hours)
113 else
113 else
114 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
114 s = issue.estimated_hours.present? ? l_hours_short(issue.estimated_hours) : ""
115 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
115 s << " (#{l(:label_total)}: #{l_hours_short(issue.total_estimated_hours)})"
116 s.html_safe
116 s.html_safe
117 end
117 end
118 end
118 end
119 end
119 end
120
120
121 def issue_spent_hours_details(issue)
121 def issue_spent_hours_details(issue)
122 if issue.total_spent_hours > 0
122 if issue.total_spent_hours > 0
123 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
123 path = project_time_entries_path(issue.project, :issue_id => "~#{issue.id}")
124
124
125 if issue.total_spent_hours == issue.spent_hours
125 if issue.total_spent_hours == issue.spent_hours
126 link_to(l_hours_short(issue.spent_hours), path)
126 link_to(l_hours_short(issue.spent_hours), path)
127 else
127 else
128 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
128 s = issue.spent_hours > 0 ? l_hours_short(issue.spent_hours) : ""
129 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
129 s << " (#{l(:label_total)}: #{link_to l_hours_short(issue.total_spent_hours), path})"
130 s.html_safe
130 s.html_safe
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 # Returns an array of error messages for bulk edited issues
135 # Returns an array of error messages for bulk edited issues
136 def bulk_edit_error_messages(issues)
136 def bulk_edit_error_messages(issues)
137 messages = {}
137 messages = {}
138 issues.each do |issue|
138 issues.each do |issue|
139 issue.errors.full_messages.each do |message|
139 issue.errors.full_messages.each do |message|
140 messages[message] ||= []
140 messages[message] ||= []
141 messages[message] << issue
141 messages[message] << issue
142 end
142 end
143 end
143 end
144 messages.map { |message, issues|
144 messages.map { |message, issues|
145 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
145 "#{message}: " + issues.map {|i| "##{i.id}"}.join(', ')
146 }
146 }
147 end
147 end
148
148
149 # Returns a link for adding a new subtask to the given issue
149 # Returns a link for adding a new subtask to the given issue
150 def link_to_new_subtask(issue)
150 def link_to_new_subtask(issue)
151 attrs = {
151 attrs = {
152 :parent_issue_id => issue
152 :parent_issue_id => issue
153 }
153 }
154 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
154 attrs[:tracker_id] = issue.tracker unless issue.tracker.disabled_core_fields.include?('parent_issue_id')
155 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
155 link_to(l(:button_add), new_project_issue_path(issue.project, :issue => attrs))
156 end
156 end
157
157
158 def trackers_options_for_select(issue)
158 def trackers_options_for_select(issue)
159 trackers = issue.allowed_target_trackers
159 trackers = issue.allowed_target_trackers
160 if issue.new_record? && issue.parent_issue_id.present?
160 if issue.new_record? && issue.parent_issue_id.present?
161 trackers = trackers.reject do |tracker|
161 trackers = trackers.reject do |tracker|
162 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
162 issue.tracker_id != tracker.id && tracker.disabled_core_fields.include?('parent_issue_id')
163 end
163 end
164 end
164 end
165 trackers.collect {|t| [t.name, t.id]}
165 trackers.collect {|t| [t.name, t.id]}
166 end
166 end
167
167
168 class IssueFieldsRows
168 class IssueFieldsRows
169 include ActionView::Helpers::TagHelper
169 include ActionView::Helpers::TagHelper
170
170
171 def initialize
171 def initialize
172 @left = []
172 @left = []
173 @right = []
173 @right = []
174 end
174 end
175
175
176 def left(*args)
176 def left(*args)
177 args.any? ? @left << cells(*args) : @left
177 args.any? ? @left << cells(*args) : @left
178 end
178 end
179
179
180 def right(*args)
180 def right(*args)
181 args.any? ? @right << cells(*args) : @right
181 args.any? ? @right << cells(*args) : @right
182 end
182 end
183
183
184 def size
184 def size
185 @left.size > @right.size ? @left.size : @right.size
185 @left.size > @right.size ? @left.size : @right.size
186 end
186 end
187
187
188 def to_html
188 def to_html
189 content =
189 content =
190 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
190 content_tag('div', @left.reduce(&:+), :class => 'splitcontentleft') +
191 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
191 content_tag('div', @right.reduce(&:+), :class => 'splitcontentleft')
192
192
193 content_tag('div', content, :class => 'splitcontent')
193 content_tag('div', content, :class => 'splitcontent')
194 end
194 end
195
195
196 def cells(label, text, options={})
196 def cells(label, text, options={})
197 options[:class] = [options[:class] || "", 'attribute'].join(' ')
197 options[:class] = [options[:class] || "", 'attribute'].join(' ')
198 content_tag 'div',
198 content_tag 'div',
199 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
199 content_tag('div', label + ":", :class => 'label') + content_tag('div', text, :class => 'value'),
200 options
200 options
201 end
201 end
202 end
202 end
203
203
204 def issue_fields_rows
204 def issue_fields_rows
205 r = IssueFieldsRows.new
205 r = IssueFieldsRows.new
206 yield r
206 yield r
207 r.to_html
207 r.to_html
208 end
208 end
209
209
210 def render_custom_fields_rows(issue)
210 def render_custom_fields_rows(issue)
211 values = issue.visible_custom_field_values
211 values = issue.visible_custom_field_values
212 return if values.empty?
212 return if values.empty?
213 half = (values.size / 2.0).ceil
213 half = (values.size / 2.0).ceil
214 issue_fields_rows do |rows|
214 issue_fields_rows do |rows|
215 values.each_with_index do |value, i|
215 values.each_with_index do |value, i|
216 css = "cf_#{value.custom_field.id}"
216 css = "cf_#{value.custom_field.id}"
217 m = (i < half ? :left : :right)
217 m = (i < half ? :left : :right)
218 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
218 rows.send m, custom_field_name_tag(value.custom_field), show_value(value), :class => css
219 end
219 end
220 end
220 end
221 end
221 end
222
222
223 # Returns the path for updating the issue form
223 # Returns the path for updating the issue form
224 # with project as the current project
224 # with project as the current project
225 def update_issue_form_path(project, issue)
225 def update_issue_form_path(project, issue)
226 options = {:format => 'js'}
226 options = {:format => 'js'}
227 if issue.new_record?
227 if issue.new_record?
228 if project
228 if project
229 new_project_issue_path(project, options)
229 new_project_issue_path(project, options)
230 else
230 else
231 new_issue_path(options)
231 new_issue_path(options)
232 end
232 end
233 else
233 else
234 edit_issue_path(issue, options)
234 edit_issue_path(issue, options)
235 end
235 end
236 end
236 end
237
237
238 # Returns the number of descendants for an array of issues
238 # Returns the number of descendants for an array of issues
239 def issues_descendant_count(issues)
239 def issues_descendant_count(issues)
240 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
240 ids = issues.reject(&:leaf?).map {|issue| issue.descendants.ids}.flatten.uniq
241 ids -= issues.map(&:id)
241 ids -= issues.map(&:id)
242 ids.size
242 ids.size
243 end
243 end
244
244
245 def issues_destroy_confirmation_message(issues)
245 def issues_destroy_confirmation_message(issues)
246 issues = [issues] unless issues.is_a?(Array)
246 issues = [issues] unless issues.is_a?(Array)
247 message = l(:text_issues_destroy_confirmation)
247 message = l(:text_issues_destroy_confirmation)
248
248
249 descendant_count = issues_descendant_count(issues)
249 descendant_count = issues_descendant_count(issues)
250 if descendant_count > 0
250 if descendant_count > 0
251 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
251 message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
252 end
252 end
253 message
253 message
254 end
254 end
255
255
256 # Returns an array of users that are proposed as watchers
256 # Returns an array of users that are proposed as watchers
257 # on the new issue form
257 # on the new issue form
258 def users_for_new_issue_watchers(issue)
258 def users_for_new_issue_watchers(issue)
259 users = issue.watcher_users
259 users = issue.watcher_users
260 if issue.project.users.count <= 20
260 if issue.project.users.count <= 20
261 users = (users + issue.project.users.sort).uniq
261 users = (users + issue.project.users.sort).uniq
262 end
262 end
263 users
263 users
264 end
264 end
265
265
266 def email_issue_attributes(issue, user)
266 def email_issue_attributes(issue, user)
267 items = []
267 items = []
268 %w(author status priority assigned_to category fixed_version).each do |attribute|
268 %w(author status priority assigned_to category fixed_version).each do |attribute|
269 unless issue.disabled_core_fields.include?(attribute+"_id")
269 unless issue.disabled_core_fields.include?(attribute+"_id")
270 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
270 items << "#{l("field_#{attribute}")}: #{issue.send attribute}"
271 end
271 end
272 end
272 end
273 issue.visible_custom_field_values(user).each do |value|
273 issue.visible_custom_field_values(user).each do |value|
274 items << "#{value.custom_field.name}: #{show_value(value, false)}"
274 items << "#{value.custom_field.name}: #{show_value(value, false)}"
275 end
275 end
276 items
276 items
277 end
277 end
278
278
279 def render_email_issue_attributes(issue, user, html=false)
279 def render_email_issue_attributes(issue, user, html=false)
280 items = email_issue_attributes(issue, user)
280 items = email_issue_attributes(issue, user)
281 if html
281 if html
282 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
282 content_tag('ul', items.map{|s| content_tag('li', s)}.join("\n").html_safe)
283 else
283 else
284 items.map{|s| "* #{s}"}.join("\n")
284 items.map{|s| "* #{s}"}.join("\n")
285 end
285 end
286 end
286 end
287
287
288 # Returns the textual representation of a journal details
288 # Returns the textual representation of a journal details
289 # as an array of strings
289 # as an array of strings
290 def details_to_strings(details, no_html=false, options={})
290 def details_to_strings(details, no_html=false, options={})
291 options[:only_path] = (options[:only_path] == false ? false : true)
291 options[:only_path] = (options[:only_path] == false ? false : true)
292 strings = []
292 strings = []
293 values_by_field = {}
293 values_by_field = {}
294 details.each do |detail|
294 details.each do |detail|
295 if detail.property == 'cf'
295 if detail.property == 'cf'
296 field = detail.custom_field
296 field = detail.custom_field
297 if field && field.multiple?
297 if field && field.multiple?
298 values_by_field[field] ||= {:added => [], :deleted => []}
298 values_by_field[field] ||= {:added => [], :deleted => []}
299 if detail.old_value
299 if detail.old_value
300 values_by_field[field][:deleted] << detail.old_value
300 values_by_field[field][:deleted] << detail.old_value
301 end
301 end
302 if detail.value
302 if detail.value
303 values_by_field[field][:added] << detail.value
303 values_by_field[field][:added] << detail.value
304 end
304 end
305 next
305 next
306 end
306 end
307 end
307 end
308 strings << show_detail(detail, no_html, options)
308 strings << show_detail(detail, no_html, options)
309 end
309 end
310 if values_by_field.present?
310 if values_by_field.present?
311 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
311 multiple_values_detail = Struct.new(:property, :prop_key, :custom_field, :old_value, :value)
312 values_by_field.each do |field, changes|
312 values_by_field.each do |field, changes|
313 if changes[:added].any?
313 if changes[:added].any?
314 detail = multiple_values_detail.new('cf', field.id.to_s, field)
314 detail = multiple_values_detail.new('cf', field.id.to_s, field)
315 detail.value = changes[:added]
315 detail.value = changes[:added]
316 strings << show_detail(detail, no_html, options)
316 strings << show_detail(detail, no_html, options)
317 end
317 end
318 if changes[:deleted].any?
318 if changes[:deleted].any?
319 detail = multiple_values_detail.new('cf', field.id.to_s, field)
319 detail = multiple_values_detail.new('cf', field.id.to_s, field)
320 detail.old_value = changes[:deleted]
320 detail.old_value = changes[:deleted]
321 strings << show_detail(detail, no_html, options)
321 strings << show_detail(detail, no_html, options)
322 end
322 end
323 end
323 end
324 end
324 end
325 strings
325 strings
326 end
326 end
327
327
328 # Returns the textual representation of a single journal detail
328 # Returns the textual representation of a single journal detail
329 def show_detail(detail, no_html=false, options={})
329 def show_detail(detail, no_html=false, options={})
330 multiple = false
330 multiple = false
331 show_diff = false
331 show_diff = false
332 no_details = false
332
333
333 case detail.property
334 case detail.property
334 when 'attr'
335 when 'attr'
335 field = detail.prop_key.to_s.gsub(/\_id$/, "")
336 field = detail.prop_key.to_s.gsub(/\_id$/, "")
336 label = l(("field_" + field).to_sym)
337 label = l(("field_" + field).to_sym)
337 case detail.prop_key
338 case detail.prop_key
338 when 'due_date', 'start_date'
339 when 'due_date', 'start_date'
339 value = format_date(detail.value.to_date) if detail.value
340 value = format_date(detail.value.to_date) if detail.value
340 old_value = format_date(detail.old_value.to_date) if detail.old_value
341 old_value = format_date(detail.old_value.to_date) if detail.old_value
341
342
342 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
343 when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
343 'priority_id', 'category_id', 'fixed_version_id'
344 'priority_id', 'category_id', 'fixed_version_id'
344 value = find_name_by_reflection(field, detail.value)
345 value = find_name_by_reflection(field, detail.value)
345 old_value = find_name_by_reflection(field, detail.old_value)
346 old_value = find_name_by_reflection(field, detail.old_value)
346
347
347 when 'estimated_hours'
348 when 'estimated_hours'
348 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
349 value = l_hours_short(detail.value.to_f) unless detail.value.blank?
349 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
350 old_value = l_hours_short(detail.old_value.to_f) unless detail.old_value.blank?
350
351
351 when 'parent_id'
352 when 'parent_id'
352 label = l(:field_parent_issue)
353 label = l(:field_parent_issue)
353 value = "##{detail.value}" unless detail.value.blank?
354 value = "##{detail.value}" unless detail.value.blank?
354 old_value = "##{detail.old_value}" unless detail.old_value.blank?
355 old_value = "##{detail.old_value}" unless detail.old_value.blank?
355
356
356 when 'is_private'
357 when 'is_private'
357 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
358 value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
358 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
359 old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
359
360
360 when 'description'
361 when 'description'
361 show_diff = true
362 show_diff = true
362 end
363 end
363 when 'cf'
364 when 'cf'
364 custom_field = detail.custom_field
365 custom_field = detail.custom_field
365 if custom_field
366 if custom_field
366 label = custom_field.name
367 label = custom_field.name
367 if custom_field.format.class.change_as_diff
368 if custom_field.format.class.change_no_details
369 no_details = true
370 elsif custom_field.format.class.change_as_diff
368 show_diff = true
371 show_diff = true
369 else
372 else
370 multiple = custom_field.multiple?
373 multiple = custom_field.multiple?
371 value = format_value(detail.value, custom_field) if detail.value
374 value = format_value(detail.value, custom_field) if detail.value
372 old_value = format_value(detail.old_value, custom_field) if detail.old_value
375 old_value = format_value(detail.old_value, custom_field) if detail.old_value
373 end
376 end
374 end
377 end
375 when 'attachment'
378 when 'attachment'
376 label = l(:label_attachment)
379 label = l(:label_attachment)
377 when 'relation'
380 when 'relation'
378 if detail.value && !detail.old_value
381 if detail.value && !detail.old_value
379 rel_issue = Issue.visible.find_by_id(detail.value)
382 rel_issue = Issue.visible.find_by_id(detail.value)
380 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
383 value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.value}" :
381 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
384 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
382 elsif detail.old_value && !detail.value
385 elsif detail.old_value && !detail.value
383 rel_issue = Issue.visible.find_by_id(detail.old_value)
386 rel_issue = Issue.visible.find_by_id(detail.old_value)
384 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
387 old_value = rel_issue.nil? ? "#{l(:label_issue)} ##{detail.old_value}" :
385 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
388 (no_html ? rel_issue : link_to_issue(rel_issue, :only_path => options[:only_path]))
386 end
389 end
387 relation_type = IssueRelation::TYPES[detail.prop_key]
390 relation_type = IssueRelation::TYPES[detail.prop_key]
388 label = l(relation_type[:name]) if relation_type
391 label = l(relation_type[:name]) if relation_type
389 end
392 end
390 call_hook(:helper_issues_show_detail_after_setting,
393 call_hook(:helper_issues_show_detail_after_setting,
391 {:detail => detail, :label => label, :value => value, :old_value => old_value })
394 {:detail => detail, :label => label, :value => value, :old_value => old_value })
392
395
393 label ||= detail.prop_key
396 label ||= detail.prop_key
394 value ||= detail.value
397 value ||= detail.value
395 old_value ||= detail.old_value
398 old_value ||= detail.old_value
396
399
397 unless no_html
400 unless no_html
398 label = content_tag('strong', label)
401 label = content_tag('strong', label)
399 old_value = content_tag("i", h(old_value)) if detail.old_value
402 old_value = content_tag("i", h(old_value)) if detail.old_value
400 if detail.old_value && detail.value.blank? && detail.property != 'relation'
403 if detail.old_value && detail.value.blank? && detail.property != 'relation'
401 old_value = content_tag("del", old_value)
404 old_value = content_tag("del", old_value)
402 end
405 end
403 if detail.property == 'attachment' && value.present? &&
406 if detail.property == 'attachment' && value.present? &&
404 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
407 atta = detail.journal.journalized.attachments.detect {|a| a.id == detail.prop_key.to_i}
405 # Link to the attachment if it has not been removed
408 # Link to the attachment if it has not been removed
406 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
409 value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
407 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
410 if options[:only_path] != false && (atta.is_text? || atta.is_image?)
408 value += ' '
411 value += ' '
409 value += link_to(l(:button_view),
412 value += link_to(l(:button_view),
410 { :controller => 'attachments', :action => 'show',
413 { :controller => 'attachments', :action => 'show',
411 :id => atta, :filename => atta.filename },
414 :id => atta, :filename => atta.filename },
412 :class => 'icon-only icon-magnifier',
415 :class => 'icon-only icon-magnifier',
413 :title => l(:button_view))
416 :title => l(:button_view))
414 end
417 end
415 else
418 else
416 value = content_tag("i", h(value)) if value
419 value = content_tag("i", h(value)) if value
417 end
420 end
418 end
421 end
419
422
420 if show_diff
423 if no_details
424 s = l(:text_journal_changed_no_detail, :label => label).html_safe
425 elsif show_diff
421 s = l(:text_journal_changed_no_detail, :label => label)
426 s = l(:text_journal_changed_no_detail, :label => label)
422 unless no_html
427 unless no_html
423 diff_link = link_to 'diff',
428 diff_link = link_to 'diff',
424 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
429 diff_journal_url(detail.journal_id, :detail_id => detail.id, :only_path => options[:only_path]),
425 :title => l(:label_view_diff)
430 :title => l(:label_view_diff)
426 s << " (#{ diff_link })"
431 s << " (#{ diff_link })"
427 end
432 end
428 s.html_safe
433 s.html_safe
429 elsif detail.value.present?
434 elsif detail.value.present?
430 case detail.property
435 case detail.property
431 when 'attr', 'cf'
436 when 'attr', 'cf'
432 if detail.old_value.present?
437 if detail.old_value.present?
433 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
438 l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
434 elsif multiple
439 elsif multiple
435 l(:text_journal_added, :label => label, :value => value).html_safe
440 l(:text_journal_added, :label => label, :value => value).html_safe
436 else
441 else
437 l(:text_journal_set_to, :label => label, :value => value).html_safe
442 l(:text_journal_set_to, :label => label, :value => value).html_safe
438 end
443 end
439 when 'attachment', 'relation'
444 when 'attachment', 'relation'
440 l(:text_journal_added, :label => label, :value => value).html_safe
445 l(:text_journal_added, :label => label, :value => value).html_safe
441 end
446 end
442 else
447 else
443 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
448 l(:text_journal_deleted, :label => label, :old => old_value).html_safe
444 end
449 end
445 end
450 end
446
451
447 # Find the name of an associated record stored in the field attribute
452 # Find the name of an associated record stored in the field attribute
448 def find_name_by_reflection(field, id)
453 def find_name_by_reflection(field, id)
449 unless id.present?
454 unless id.present?
450 return nil
455 return nil
451 end
456 end
452 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
457 @detail_value_name_by_reflection ||= Hash.new do |hash, key|
453 association = Issue.reflect_on_association(key.first.to_sym)
458 association = Issue.reflect_on_association(key.first.to_sym)
454 name = nil
459 name = nil
455 if association
460 if association
456 record = association.klass.find_by_id(key.last)
461 record = association.klass.find_by_id(key.last)
457 if record
462 if record
458 name = record.name.force_encoding('UTF-8')
463 name = record.name.force_encoding('UTF-8')
459 end
464 end
460 end
465 end
461 hash[key] = name
466 hash[key] = name
462 end
467 end
463 @detail_value_name_by_reflection[[field, id]]
468 @detail_value_name_by_reflection[[field, id]]
464 end
469 end
465
470
466 # Renders issue children recursively
471 # Renders issue children recursively
467 def render_api_issue_children(issue, api)
472 def render_api_issue_children(issue, api)
468 return if issue.leaf?
473 return if issue.leaf?
469 api.array :children do
474 api.array :children do
470 issue.children.each do |child|
475 issue.children.each do |child|
471 api.issue(:id => child.id) do
476 api.issue(:id => child.id) do
472 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
477 api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
473 api.subject child.subject
478 api.subject child.subject
474 render_api_issue_children(child, api)
479 render_api_issue_children(child, api)
475 end
480 end
476 end
481 end
477 end
482 end
478 end
483 end
479 end
484 end
@@ -1,315 +1,326
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 CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::SubclassFactory
20 include Redmine::SubclassFactory
21
21
22 has_many :enumerations,
22 has_many :enumerations,
23 lambda { order(:position) },
23 lambda { order(:position) },
24 :class_name => 'CustomFieldEnumeration',
24 :class_name => 'CustomFieldEnumeration',
25 :dependent => :delete_all
25 :dependent => :delete_all
26 has_many :custom_values, :dependent => :delete_all
26 has_many :custom_values, :dependent => :delete_all
27 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
28 acts_as_positioned
28 acts_as_positioned
29 serialize :possible_values
29 serialize :possible_values
30 store :format_store
30 store :format_store
31
31
32 validates_presence_of :name, :field_format
32 validates_presence_of :name, :field_format
33 validates_uniqueness_of :name, :scope => :type
33 validates_uniqueness_of :name, :scope => :type
34 validates_length_of :name, :maximum => 30
34 validates_length_of :name, :maximum => 30
35 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
35 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
36 validate :validate_custom_field
36 validate :validate_custom_field
37 attr_protected :id
37 attr_protected :id
38
38
39 before_validation :set_searchable
39 before_validation :set_searchable
40 before_save do |field|
40 before_save do |field|
41 field.format.before_custom_field_save(field)
41 field.format.before_custom_field_save(field)
42 end
42 end
43 after_save :handle_multiplicity_change
43 after_save :handle_multiplicity_change
44 after_save do |field|
44 after_save do |field|
45 if field.visible_changed? && field.visible
45 if field.visible_changed? && field.visible
46 field.roles.clear
46 field.roles.clear
47 end
47 end
48 end
48 end
49
49
50 scope :sorted, lambda { order(:position) }
50 scope :sorted, lambda { order(:position) }
51 scope :visible, lambda {|*args|
51 scope :visible, lambda {|*args|
52 user = args.shift || User.current
52 user = args.shift || User.current
53 if user.admin?
53 if user.admin?
54 # nop
54 # nop
55 elsif user.memberships.any?
55 elsif user.memberships.any?
56 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
56 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
57 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
57 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
58 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
58 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
59 " WHERE m.user_id = ?)",
59 " WHERE m.user_id = ?)",
60 true, user.id)
60 true, user.id)
61 else
61 else
62 where(:visible => true)
62 where(:visible => true)
63 end
63 end
64 }
64 }
65 def visible_by?(project, user=User.current)
65 def visible_by?(project, user=User.current)
66 visible? || user.admin?
66 visible? || user.admin?
67 end
67 end
68
68
69 safe_attributes 'name',
69 safe_attributes 'name',
70 'field_format',
70 'field_format',
71 'possible_values',
71 'possible_values',
72 'regexp',
72 'regexp',
73 'min_lnegth',
73 'min_lnegth',
74 'max_length',
74 'max_length',
75 'is_required',
75 'is_required',
76 'is_for_all',
76 'is_for_all',
77 'is_filter',
77 'is_filter',
78 'position',
78 'position',
79 'searchable',
79 'searchable',
80 'default_value',
80 'default_value',
81 'editable',
81 'editable',
82 'visible',
82 'visible',
83 'multiple',
83 'multiple',
84 'description',
84 'description',
85 'role_ids',
85 'role_ids',
86 'url_pattern',
86 'url_pattern',
87 'text_formatting',
87 'text_formatting',
88 'edit_tag_style',
88 'edit_tag_style',
89 'user_role',
89 'user_role',
90 'version_status'
90 'version_status'
91
91
92 def format
92 def format
93 @format ||= Redmine::FieldFormat.find(field_format)
93 @format ||= Redmine::FieldFormat.find(field_format)
94 end
94 end
95
95
96 def field_format=(arg)
96 def field_format=(arg)
97 # cannot change format of a saved custom field
97 # cannot change format of a saved custom field
98 if new_record?
98 if new_record?
99 @format = nil
99 @format = nil
100 super
100 super
101 end
101 end
102 end
102 end
103
103
104 def set_searchable
104 def set_searchable
105 # make sure these fields are not searchable
105 # make sure these fields are not searchable
106 self.searchable = false unless format.class.searchable_supported
106 self.searchable = false unless format.class.searchable_supported
107 # make sure only these fields can have multiple values
107 # make sure only these fields can have multiple values
108 self.multiple = false unless format.class.multiple_supported
108 self.multiple = false unless format.class.multiple_supported
109 true
109 true
110 end
110 end
111
111
112 def validate_custom_field
112 def validate_custom_field
113 format.validate_custom_field(self).each do |attribute, message|
113 format.validate_custom_field(self).each do |attribute, message|
114 errors.add attribute, message
114 errors.add attribute, message
115 end
115 end
116
116
117 if regexp.present?
117 if regexp.present?
118 begin
118 begin
119 Regexp.new(regexp)
119 Regexp.new(regexp)
120 rescue
120 rescue
121 errors.add(:regexp, :invalid)
121 errors.add(:regexp, :invalid)
122 end
122 end
123 end
123 end
124
124
125 if default_value.present?
125 if default_value.present?
126 validate_field_value(default_value).each do |message|
126 validate_field_value(default_value).each do |message|
127 errors.add :default_value, message
127 errors.add :default_value, message
128 end
128 end
129 end
129 end
130 end
130 end
131
131
132 def possible_custom_value_options(custom_value)
132 def possible_custom_value_options(custom_value)
133 format.possible_custom_value_options(custom_value)
133 format.possible_custom_value_options(custom_value)
134 end
134 end
135
135
136 def possible_values_options(object=nil)
136 def possible_values_options(object=nil)
137 if object.is_a?(Array)
137 if object.is_a?(Array)
138 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
138 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
139 else
139 else
140 format.possible_values_options(self, object) || []
140 format.possible_values_options(self, object) || []
141 end
141 end
142 end
142 end
143
143
144 def possible_values
144 def possible_values
145 values = read_attribute(:possible_values)
145 values = read_attribute(:possible_values)
146 if values.is_a?(Array)
146 if values.is_a?(Array)
147 values.each do |value|
147 values.each do |value|
148 value.to_s.force_encoding('UTF-8')
148 value.to_s.force_encoding('UTF-8')
149 end
149 end
150 values
150 values
151 else
151 else
152 []
152 []
153 end
153 end
154 end
154 end
155
155
156 # Makes possible_values accept a multiline string
156 # Makes possible_values accept a multiline string
157 def possible_values=(arg)
157 def possible_values=(arg)
158 if arg.is_a?(Array)
158 if arg.is_a?(Array)
159 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
159 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
160 write_attribute(:possible_values, values)
160 write_attribute(:possible_values, values)
161 else
161 else
162 self.possible_values = arg.to_s.split(/[\n\r]+/)
162 self.possible_values = arg.to_s.split(/[\n\r]+/)
163 end
163 end
164 end
164 end
165
165
166 def set_custom_field_value(custom_field_value, value)
167 format.set_custom_field_value(self, custom_field_value, value)
168 end
169
166 def cast_value(value)
170 def cast_value(value)
167 format.cast_value(self, value)
171 format.cast_value(self, value)
168 end
172 end
169
173
170 def value_from_keyword(keyword, customized)
174 def value_from_keyword(keyword, customized)
171 format.value_from_keyword(self, keyword, customized)
175 format.value_from_keyword(self, keyword, customized)
172 end
176 end
173
177
174 # Returns the options hash used to build a query filter for the field
178 # Returns the options hash used to build a query filter for the field
175 def query_filter_options(query)
179 def query_filter_options(query)
176 format.query_filter_options(self, query)
180 format.query_filter_options(self, query)
177 end
181 end
178
182
179 def totalable?
183 def totalable?
180 format.totalable_supported
184 format.totalable_supported
181 end
185 end
182
186
183 # Returns a ORDER BY clause that can used to sort customized
187 # Returns a ORDER BY clause that can used to sort customized
184 # objects by their value of the custom field.
188 # objects by their value of the custom field.
185 # Returns nil if the custom field can not be used for sorting.
189 # Returns nil if the custom field can not be used for sorting.
186 def order_statement
190 def order_statement
187 return nil if multiple?
191 return nil if multiple?
188 format.order_statement(self)
192 format.order_statement(self)
189 end
193 end
190
194
191 # Returns a GROUP BY clause that can used to group by custom value
195 # Returns a GROUP BY clause that can used to group by custom value
192 # Returns nil if the custom field can not be used for grouping.
196 # Returns nil if the custom field can not be used for grouping.
193 def group_statement
197 def group_statement
194 return nil if multiple?
198 return nil if multiple?
195 format.group_statement(self)
199 format.group_statement(self)
196 end
200 end
197
201
198 def join_for_order_statement
202 def join_for_order_statement
199 format.join_for_order_statement(self)
203 format.join_for_order_statement(self)
200 end
204 end
201
205
202 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
206 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
203 if visible? || user.admin?
207 if visible? || user.admin?
204 "1=1"
208 "1=1"
205 elsif user.anonymous?
209 elsif user.anonymous?
206 "1=0"
210 "1=0"
207 else
211 else
208 project_key ||= "#{self.class.customized_class.table_name}.project_id"
212 project_key ||= "#{self.class.customized_class.table_name}.project_id"
209 id_column ||= id
213 id_column ||= id
210 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
214 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
211 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
215 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
212 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
216 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
213 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
217 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
214 end
218 end
215 end
219 end
216
220
217 def self.visibility_condition
221 def self.visibility_condition
218 if user.admin?
222 if user.admin?
219 "1=1"
223 "1=1"
220 elsif user.anonymous?
224 elsif user.anonymous?
221 "#{table_name}.visible"
225 "#{table_name}.visible"
222 else
226 else
223 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
227 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
224 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
228 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
225 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
229 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
226 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
230 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
227 end
231 end
228 end
232 end
229
233
230 def <=>(field)
234 def <=>(field)
231 position <=> field.position
235 position <=> field.position
232 end
236 end
233
237
234 # Returns the class that values represent
238 # Returns the class that values represent
235 def value_class
239 def value_class
236 format.target_class if format.respond_to?(:target_class)
240 format.target_class if format.respond_to?(:target_class)
237 end
241 end
238
242
239 def self.customized_class
243 def self.customized_class
240 self.name =~ /^(.+)CustomField$/
244 self.name =~ /^(.+)CustomField$/
241 $1.constantize rescue nil
245 $1.constantize rescue nil
242 end
246 end
243
247
244 # to move in project_custom_field
248 # to move in project_custom_field
245 def self.for_all
249 def self.for_all
246 where(:is_for_all => true).order('position').to_a
250 where(:is_for_all => true).order('position').to_a
247 end
251 end
248
252
249 def type_name
253 def type_name
250 nil
254 nil
251 end
255 end
252
256
253 # Returns the error messages for the given value
257 # Returns the error messages for the given value
254 # or an empty array if value is a valid value for the custom field
258 # or an empty array if value is a valid value for the custom field
255 def validate_custom_value(custom_value)
259 def validate_custom_value(custom_value)
256 value = custom_value.value
260 value = custom_value.value
257 errs = []
261 errs = format.validate_custom_value(custom_value)
258 if value.is_a?(Array)
262
259 if !multiple?
263 unless errs.any?
260 errs << ::I18n.t('activerecord.errors.messages.invalid')
264 if value.is_a?(Array)
261 end
265 if !multiple?
262 if is_required? && value.detect(&:present?).nil?
266 errs << ::I18n.t('activerecord.errors.messages.invalid')
263 errs << ::I18n.t('activerecord.errors.messages.blank')
267 end
264 end
268 if is_required? && value.detect(&:present?).nil?
265 else
269 errs << ::I18n.t('activerecord.errors.messages.blank')
266 if is_required? && value.blank?
270 end
267 errs << ::I18n.t('activerecord.errors.messages.blank')
271 else
272 if is_required? && value.blank?
273 errs << ::I18n.t('activerecord.errors.messages.blank')
274 end
268 end
275 end
269 end
276 end
270 errs += format.validate_custom_value(custom_value)
277
271 errs
278 errs
272 end
279 end
273
280
274 # Returns the error messages for the default custom field value
281 # Returns the error messages for the default custom field value
275 def validate_field_value(value)
282 def validate_field_value(value)
276 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
283 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
277 end
284 end
278
285
279 # Returns true if value is a valid value for the custom field
286 # Returns true if value is a valid value for the custom field
280 def valid_field_value?(value)
287 def valid_field_value?(value)
281 validate_field_value(value).empty?
288 validate_field_value(value).empty?
282 end
289 end
283
290
291 def after_save_custom_value(custom_value)
292 format.after_save_custom_value(self, custom_value)
293 end
294
284 def format_in?(*args)
295 def format_in?(*args)
285 args.include?(field_format)
296 args.include?(field_format)
286 end
297 end
287
298
288 def self.human_attribute_name(attribute_key_name, *args)
299 def self.human_attribute_name(attribute_key_name, *args)
289 attr_name = attribute_key_name.to_s
300 attr_name = attribute_key_name.to_s
290 if attr_name == 'url_pattern'
301 if attr_name == 'url_pattern'
291 attr_name = "url"
302 attr_name = "url"
292 end
303 end
293 super(attr_name, *args)
304 super(attr_name, *args)
294 end
305 end
295
306
296 protected
307 protected
297
308
298 # Removes multiple values for the custom field after setting the multiple attribute to false
309 # Removes multiple values for the custom field after setting the multiple attribute to false
299 # We kepp the value with the highest id for each customized object
310 # We kepp the value with the highest id for each customized object
300 def handle_multiplicity_change
311 def handle_multiplicity_change
301 if !new_record? && multiple_was && !multiple
312 if !new_record? && multiple_was && !multiple
302 ids = custom_values.
313 ids = custom_values.
303 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
314 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
304 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
315 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
305 " AND cve.id > #{CustomValue.table_name}.id)").
316 " AND cve.id > #{CustomValue.table_name}.id)").
306 pluck(:id)
317 pluck(:id)
307
318
308 if ids.any?
319 if ids.any?
309 custom_values.where(:id => ids).delete_all
320 custom_values.where(:id => ids).delete_all
310 end
321 end
311 end
322 end
312 end
323 end
313 end
324 end
314
325
315 require_dependency 'redmine/field_format'
326 require_dependency 'redmine/field_format'
@@ -1,56 +1,60
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 CustomFieldValue
18 class CustomFieldValue
19 attr_accessor :custom_field, :customized, :value, :value_was
19 attr_accessor :custom_field, :customized, :value, :value_was
20
20
21 def initialize(attributes={})
21 def initialize(attributes={})
22 attributes.each do |name, v|
22 attributes.each do |name, v|
23 send "#{name}=", v
23 send "#{name}=", v
24 end
24 end
25 end
25 end
26
26
27 def custom_field_id
27 def custom_field_id
28 custom_field.id
28 custom_field.id
29 end
29 end
30
30
31 def true?
31 def true?
32 self.value == '1'
32 self.value == '1'
33 end
33 end
34
34
35 def editable?
35 def editable?
36 custom_field.editable?
36 custom_field.editable?
37 end
37 end
38
38
39 def visible?
39 def visible?
40 custom_field.visible?
40 custom_field.visible?
41 end
41 end
42
42
43 def required?
43 def required?
44 custom_field.is_required?
44 custom_field.is_required?
45 end
45 end
46
46
47 def to_s
47 def to_s
48 value.to_s
48 value.to_s
49 end
49 end
50
50
51 def value=(v)
52 @value = custom_field.set_custom_field_value(self, v)
53 end
54
51 def validate_value
55 def validate_value
52 custom_field.validate_custom_value(self).each do |message|
56 custom_field.validate_custom_value(self).each do |message|
53 customized.errors.add(:base, custom_field.name + ' ' + message)
57 customized.errors.add(:base, custom_field.name + ' ' + message)
54 end
58 end
55 end
59 end
56 end
60 end
@@ -1,50 +1,62
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 CustomValue < ActiveRecord::Base
18 class CustomValue < ActiveRecord::Base
19 belongs_to :custom_field
19 belongs_to :custom_field
20 belongs_to :customized, :polymorphic => true
20 belongs_to :customized, :polymorphic => true
21 attr_protected :id
21 attr_protected :id
22
22
23 after_save :custom_field_after_save_custom_value
24
23 def initialize(attributes=nil, *args)
25 def initialize(attributes=nil, *args)
24 super
26 super
25 if new_record? && custom_field && !attributes.key?(:value)
27 if new_record? && custom_field && !attributes.key?(:value)
26 self.value ||= custom_field.default_value
28 self.value ||= custom_field.default_value
27 end
29 end
28 end
30 end
29
31
30 # Returns true if the boolean custom value is true
32 # Returns true if the boolean custom value is true
31 def true?
33 def true?
32 self.value == '1'
34 self.value == '1'
33 end
35 end
34
36
35 def editable?
37 def editable?
36 custom_field.editable?
38 custom_field.editable?
37 end
39 end
38
40
39 def visible?
41 def visible?
40 custom_field.visible?
42 custom_field.visible?
41 end
43 end
42
44
45 def attachments_visible?(user)
46 visible? && customized && customized.visible?(user)
47 end
48
43 def required?
49 def required?
44 custom_field.is_required?
50 custom_field.is_required?
45 end
51 end
46
52
47 def to_s
53 def to_s
48 value.to_s
54 value.to_s
49 end
55 end
56
57 private
58
59 def custom_field_after_save_custom_value
60 custom_field.after_save_custom_value(self)
61 end
50 end
62 end
@@ -1,31 +1,47
1 <span id="attachments_fields">
1 <% attachment_param ||= 'attachments' %>
2 <% if defined?(container) && container && container.saved_attachments %>
2 <% saved_attachments ||= container.saved_attachments if defined?(container) && container %>
3 <% container.saved_attachments.each_with_index do |attachment, i| %>
3 <% multiple = true unless defined?(multiple) && multiple == false %>
4 <span id="attachments_p<%= i %>">
4 <% show_add = multiple || saved_attachments.blank? %>
5 <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') +
5 <% description = (defined?(description) && description == false ? false : true) %>
6 text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') +
6 <% css_class = (defined?(filedrop) && filedrop == false ? '' : 'filedrop') %>
7 link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
7
8 <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %>
8 <span class="attachments_form">
9 </span>
9 <span class="attachments_fields">
10 <% if saved_attachments.present? %>
11 <% saved_attachments.each_with_index do |attachment, i| %>
12 <span id="attachments_p<%= i %>">
13 <%= text_field_tag("#{attachment_param}[p#{i}][filename]", attachment.filename, :class => 'filename') %>
14 <% if attachment.container_id.present? %>
15 <%= link_to l(:label_delete), "#", :onclick => "$(this).closest('.attachments_form').find('.add_attachment').show(); $(this).parent().remove(); return false;", :class => 'icon-only icon-del' %>
16 <%= hidden_field_tag "#{attachment_param}[p#{i}][id]", attachment.id %>
17 <% else %>
18 <%= text_field_tag("#{attachment_param}[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') if description %>
19 <%= link_to('&nbsp;'.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %>
20 <%= hidden_field_tag "#{attachment_param}[p#{i}][token]", attachment.token %>
21 <% end %>
22 </span>
23 <% end %>
10 <% end %>
24 <% end %>
11 <% end %>
25 </span>
12 </span>
26 <span class="add_attachment" style="<%= show_add ? nil : 'display:none;' %>">
13 <span class="add_attachment">
27 <%= file_field_tag "#{attachment_param}[dummy][file]",
14 <%= file_field_tag 'attachments[dummy][file]',
28 :id => nil,
15 :id => nil,
29 :class => "file_selector #{css_class}",
16 :class => 'file_selector',
30 :multiple => multiple,
17 :multiple => true,
31 :onchange => 'addInputFiles(this);',
18 :onchange => 'addInputFiles(this);',
32 :data => {
19 :data => {
33 :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
20 :max_file_size => Setting.attachment_max_size.to_i.kilobytes,
34 :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
21 :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)),
35 :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
22 :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i,
36 :upload_path => uploads_path(:format => 'js'),
23 :upload_path => uploads_path(:format => 'js'),
37 :param => attachment_param,
24 :description_placeholder => l(:label_optional_description)
38 :description => description,
25 } %>
39 :description_placeholder => l(:label_optional_description)
26 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
40 } %>
41 (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)
42 </span>
27 </span>
43 </span>
28
44
29 <% content_for :header_tags do %>
45 <% content_for :header_tags do %>
30 <%= javascript_include_tag 'attachments' %>
46 <%= javascript_include_tag 'attachments' %>
31 <% end %>
47 <% end %>
@@ -1,1 +1,2
1 $('#attachments_<%= j params[:attachment_id] %>').closest('.attachments_form').find('.add_attachment').show();
1 $('#attachments_<%= j params[:attachment_id] %>').remove();
2 $('#attachments_<%= j params[:attachment_id] %>').remove();
@@ -1,14 +1,14
1 var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
1 var fileSpan = $('#attachments_<%= j params[:attachment_id] %>');
2 <% if @attachment.new_record? %>
2 <% if @attachment.new_record? %>
3 fileSpan.hide();
3 fileSpan.hide();
4 alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
4 alert("<%= escape_javascript @attachment.errors.full_messages.join(', ') %>");
5 <% else %>
5 <% else %>
6 $('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan);
6 fileSpan.find('input.token').val('<%= j @attachment.token %>');
7 fileSpan.find('a.remove-upload')
7 fileSpan.find('a.remove-upload')
8 .attr({
8 .attr({
9 "data-remote": true,
9 "data-remote": true,
10 "data-method": 'delete',
10 "data-method": 'delete',
11 href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
11 href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>'
12 })
12 })
13 .off('click');
13 .off('click');
14 <% end %>
14 <% end %>
@@ -1,115 +1,127
1 <%= error_messages_for 'custom_field' %>
1 <%= error_messages_for 'custom_field' %>
2
2
3 <div class="splitcontentleft">
3 <div class="splitcontentleft">
4 <div class="box tabular">
4 <div class="box tabular">
5 <p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :disabled => !@custom_field.new_record? %></p>
5 <p><%= f.select :field_format, custom_field_formats_for_select(@custom_field), {}, :disabled => !@custom_field.new_record? %></p>
6 <p><%= f.text_field :name, :size => 50, :required => true %></p>
6 <p><%= f.text_field :name, :size => 50, :required => true %></p>
7 <p><%= f.text_area :description, :rows => 7 %></p>
7 <p><%= f.text_area :description, :rows => 7 %></p>
8
8
9 <% if @custom_field.format.multiple_supported %>
9 <% if @custom_field.format.multiple_supported %>
10 <p>
10 <p>
11 <%= f.check_box :multiple %>
11 <%= f.check_box :multiple %>
12 <% if !@custom_field.new_record? && @custom_field.multiple %>
12 <% if !@custom_field.new_record? && @custom_field.multiple %>
13 <em class="info"><%= l(:text_turning_multiple_off) %></em>
13 <em class="info"><%= l(:text_turning_multiple_off) %></em>
14 <% end %>
14 <% end %>
15 </p>
15 </p>
16 <% end %>
16 <% end %>
17
17
18 <%= render_custom_field_format_partial f, @custom_field %>
18 <%= render_custom_field_format_partial f, @custom_field %>
19
19
20 <%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
20 <%= call_hook(:view_custom_fields_form_upper_box, :custom_field => @custom_field, :form => f) %>
21 </div>
21 </div>
22 <p><%= submit_tag l(:button_save) %></p>
22 <p><%= submit_tag l(:button_save) %></p>
23 </div>
23 </div>
24
24
25 <div class="splitcontentright">
25 <div class="splitcontentright">
26 <div class="box tabular">
26 <div class="box tabular">
27 <% case @custom_field.class.name
27 <% case @custom_field.class.name
28 when "IssueCustomField" %>
28 when "IssueCustomField" %>
29 <p><%= f.check_box :is_required %></p>
29 <p><%= f.check_box :is_required %></p>
30 <p><%= f.check_box :is_for_all, :data => {:disables => '#custom_field_project_ids input'} %></p>
30 <p><%= f.check_box :is_for_all, :data => {:disables => '#custom_field_project_ids input'} %></p>
31 <% if @custom_field.format.is_filter_supported %>
31 <p><%= f.check_box :is_filter %></p>
32 <p><%= f.check_box :is_filter %></p>
33 <% end %>
32 <% if @custom_field.format.searchable_supported %>
34 <% if @custom_field.format.searchable_supported %>
33 <p><%= f.check_box :searchable %></p>
35 <p><%= f.check_box :searchable %></p>
34 <% end %>
36 <% end %>
35 <p>
37 <p>
36 <label><%= l(:field_visible) %></label>
38 <label><%= l(:field_visible) %></label>
37 <label class="block">
39 <label class="block">
38 <%= radio_button_tag 'custom_field[visible]', 1, @custom_field.visible?, :id => 'custom_field_visible_on',
40 <%= radio_button_tag 'custom_field[visible]', 1, @custom_field.visible?, :id => 'custom_field_visible_on',
39 :data => {:disables => '.custom_field_role input'} %>
41 :data => {:disables => '.custom_field_role input'} %>
40 <%= l(:label_visibility_public) %>
42 <%= l(:label_visibility_public) %>
41 </label>
43 </label>
42 <label class="block">
44 <label class="block">
43 <%= radio_button_tag 'custom_field[visible]', 0, !@custom_field.visible?, :id => 'custom_field_visible_off',
45 <%= radio_button_tag 'custom_field[visible]', 0, !@custom_field.visible?, :id => 'custom_field_visible_off',
44 :data => {:enables => '.custom_field_role input'} %>
46 :data => {:enables => '.custom_field_role input'} %>
45 <%= l(:label_visibility_roles) %>:
47 <%= l(:label_visibility_roles) %>:
46 </label>
48 </label>
47 <% Role.givable.sorted.each do |role| %>
49 <% Role.givable.sorted.each do |role| %>
48 <label class="block custom_field_role" style="padding-left:2em;">
50 <label class="block custom_field_role" style="padding-left:2em;">
49 <%= check_box_tag 'custom_field[role_ids][]', role.id, @custom_field.roles.include?(role), :id => nil %>
51 <%= check_box_tag 'custom_field[role_ids][]', role.id, @custom_field.roles.include?(role), :id => nil %>
50 <%= role.name %>
52 <%= role.name %>
51 </label>
53 </label>
52 <% end %>
54 <% end %>
53 <%= hidden_field_tag 'custom_field[role_ids][]', '' %>
55 <%= hidden_field_tag 'custom_field[role_ids][]', '' %>
54 </p>
56 </p>
55
57
56 <% when "UserCustomField" %>
58 <% when "UserCustomField" %>
57 <p><%= f.check_box :is_required %></p>
59 <p><%= f.check_box :is_required %></p>
58 <p><%= f.check_box :visible %></p>
60 <p><%= f.check_box :visible %></p>
59 <p><%= f.check_box :editable %></p>
61 <p><%= f.check_box :editable %></p>
62 <% if @custom_field.format.is_filter_supported %>
60 <p><%= f.check_box :is_filter %></p>
63 <p><%= f.check_box :is_filter %></p>
64 <% end %>
61
65
62 <% when "ProjectCustomField" %>
66 <% when "ProjectCustomField" %>
63 <p><%= f.check_box :is_required %></p>
67 <p><%= f.check_box :is_required %></p>
64 <p><%= f.check_box :visible %></p>
68 <p><%= f.check_box :visible %></p>
65 <% if @custom_field.format.searchable_supported %>
69 <% if @custom_field.format.searchable_supported %>
66 <p><%= f.check_box :searchable %></p>
70 <p><%= f.check_box :searchable %></p>
67 <% end %>
71 <% end %>
72 <% if @custom_field.format.is_filter_supported %>
68 <p><%= f.check_box :is_filter %></p>
73 <p><%= f.check_box :is_filter %></p>
74 <% end %>
69
75
70 <% when "VersionCustomField" %>
76 <% when "VersionCustomField" %>
71 <p><%= f.check_box :is_required %></p>
77 <p><%= f.check_box :is_required %></p>
78 <% if @custom_field.format.is_filter_supported %>
72 <p><%= f.check_box :is_filter %></p>
79 <p><%= f.check_box :is_filter %></p>
80 <% end %>
73
81
74 <% when "GroupCustomField" %>
82 <% when "GroupCustomField" %>
75 <p><%= f.check_box :is_required %></p>
83 <p><%= f.check_box :is_required %></p>
84 <% if @custom_field.format.is_filter_supported %>
76 <p><%= f.check_box :is_filter %></p>
85 <p><%= f.check_box :is_filter %></p>
86 <% end %>
77
87
78 <% when "TimeEntryCustomField" %>
88 <% when "TimeEntryCustomField" %>
79 <p><%= f.check_box :is_required %></p>
89 <p><%= f.check_box :is_required %></p>
90 <% if @custom_field.format.is_filter_supported %>
80 <p><%= f.check_box :is_filter %></p>
91 <p><%= f.check_box :is_filter %></p>
92 <% end %>
81
93
82 <% else %>
94 <% else %>
83 <p><%= f.check_box :is_required %></p>
95 <p><%= f.check_box :is_required %></p>
84
96
85 <% end %>
97 <% end %>
86 <%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
98 <%= call_hook(:"view_custom_fields_form_#{@custom_field.type.to_s.underscore}", :custom_field => @custom_field, :form => f) %>
87 </div>
99 </div>
88
100
89 <% if @custom_field.is_a?(IssueCustomField) %>
101 <% if @custom_field.is_a?(IssueCustomField) %>
90 <fieldset class="box" id="custom_field_tracker_ids"><legend><%=l(:label_tracker_plural)%></legend>
102 <fieldset class="box" id="custom_field_tracker_ids"><legend><%=l(:label_tracker_plural)%></legend>
91 <% Tracker.sorted.each do |tracker| %>
103 <% Tracker.sorted.each do |tracker| %>
92 <%= check_box_tag "custom_field[tracker_ids][]",
104 <%= check_box_tag "custom_field[tracker_ids][]",
93 tracker.id,
105 tracker.id,
94 (@custom_field.trackers.include? tracker),
106 (@custom_field.trackers.include? tracker),
95 :id => "custom_field_tracker_ids_#{tracker.id}" %>
107 :id => "custom_field_tracker_ids_#{tracker.id}" %>
96 <label class="no-css" for="custom_field_tracker_ids_<%=tracker.id%>">
108 <label class="no-css" for="custom_field_tracker_ids_<%=tracker.id%>">
97 <%= tracker.name %>
109 <%= tracker.name %>
98 </label>
110 </label>
99 <% end %>
111 <% end %>
100 <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
112 <%= hidden_field_tag "custom_field[tracker_ids][]", '' %>
101 <p><%= check_all_links 'custom_field_tracker_ids' %></p>
113 <p><%= check_all_links 'custom_field_tracker_ids' %></p>
102 </fieldset>
114 </fieldset>
103
115
104 <fieldset class="box" id="custom_field_project_ids"><legend><%= l(:label_project_plural) %></legend>
116 <fieldset class="box" id="custom_field_project_ids"><legend><%= l(:label_project_plural) %></legend>
105 <% project_ids = @custom_field.project_ids.to_a %>
117 <% project_ids = @custom_field.project_ids.to_a %>
106 <%= render_project_nested_lists(Project.all) do |p|
118 <%= render_project_nested_lists(Project.all) do |p|
107 content_tag('label', check_box_tag('custom_field[project_ids][]', p.id, project_ids.include?(p.id), :id => nil) + ' ' + p)
119 content_tag('label', check_box_tag('custom_field[project_ids][]', p.id, project_ids.include?(p.id), :id => nil) + ' ' + p)
108 end %>
120 end %>
109 <%= hidden_field_tag('custom_field[project_ids][]', '', :id => nil) %>
121 <%= hidden_field_tag('custom_field[project_ids][]', '', :id => nil) %>
110 <p><%= check_all_links 'custom_field_project_ids' %></p>
122 <p><%= check_all_links 'custom_field_project_ids' %></p>
111 </fieldset>
123 </fieldset>
112 <% end %>
124 <% end %>
113 </div>
125 </div>
114
126
115 <% include_calendar_headers_tags %>
127 <% include_calendar_headers_tags %>
@@ -1,131 +1,140
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 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Attachable
20 module Attachable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_attachable(options = {})
26 def acts_as_attachable(options = {})
27 cattr_accessor :attachable_options
27 cattr_accessor :attachable_options
28 self.attachable_options = {}
28 self.attachable_options = {}
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
29 attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:edit_permission] = options.delete(:edit_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
30 attachable_options[:edit_permission] = options.delete(:edit_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
31 attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
32
32
33 has_many :attachments, lambda {order("#{Attachment.table_name}.created_on ASC, #{Attachment.table_name}.id ASC")},
33 has_many :attachments, lambda {order("#{Attachment.table_name}.created_on ASC, #{Attachment.table_name}.id ASC")},
34 options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
34 options.merge(:as => :container, :dependent => :destroy, :inverse_of => :container)
35 send :include, Redmine::Acts::Attachable::InstanceMethods
35 send :include, Redmine::Acts::Attachable::InstanceMethods
36 before_save :attach_saved_attachments
36 before_save :attach_saved_attachments
37 after_rollback :detach_saved_attachments
37 validate :warn_about_failed_attachments
38 validate :warn_about_failed_attachments
38 end
39 end
39 end
40 end
40
41
41 module InstanceMethods
42 module InstanceMethods
42 def self.included(base)
43 def self.included(base)
43 base.extend ClassMethods
44 base.extend ClassMethods
44 end
45 end
45
46
46 def attachments_visible?(user=User.current)
47 def attachments_visible?(user=User.current)
47 (respond_to?(:visible?) ? visible?(user) : true) &&
48 (respond_to?(:visible?) ? visible?(user) : true) &&
48 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
49 user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
49 end
50 end
50
51
51 def attachments_editable?(user=User.current)
52 def attachments_editable?(user=User.current)
52 (respond_to?(:visible?) ? visible?(user) : true) &&
53 (respond_to?(:visible?) ? visible?(user) : true) &&
53 user.allowed_to?(self.class.attachable_options[:edit_permission], self.project)
54 user.allowed_to?(self.class.attachable_options[:edit_permission], self.project)
54 end
55 end
55
56
56 def attachments_deletable?(user=User.current)
57 def attachments_deletable?(user=User.current)
57 (respond_to?(:visible?) ? visible?(user) : true) &&
58 (respond_to?(:visible?) ? visible?(user) : true) &&
58 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
59 user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
59 end
60 end
60
61
61 def saved_attachments
62 def saved_attachments
62 @saved_attachments ||= []
63 @saved_attachments ||= []
63 end
64 end
64
65
65 def unsaved_attachments
66 def unsaved_attachments
66 @unsaved_attachments ||= []
67 @unsaved_attachments ||= []
67 end
68 end
68
69
69 def save_attachments(attachments, author=User.current)
70 def save_attachments(attachments, author=User.current)
70 if attachments.is_a?(Hash)
71 if attachments.is_a?(Hash)
71 attachments = attachments.stringify_keys
72 attachments = attachments.stringify_keys
72 attachments = attachments.to_a.sort {|a, b|
73 attachments = attachments.to_a.sort {|a, b|
73 if a.first.to_i > 0 && b.first.to_i > 0
74 if a.first.to_i > 0 && b.first.to_i > 0
74 a.first.to_i <=> b.first.to_i
75 a.first.to_i <=> b.first.to_i
75 elsif a.first.to_i > 0
76 elsif a.first.to_i > 0
76 1
77 1
77 elsif b.first.to_i > 0
78 elsif b.first.to_i > 0
78 -1
79 -1
79 else
80 else
80 a.first <=> b.first
81 a.first <=> b.first
81 end
82 end
82 }
83 }
83 attachments = attachments.map(&:last)
84 attachments = attachments.map(&:last)
84 end
85 end
85 if attachments.is_a?(Array)
86 if attachments.is_a?(Array)
86 @failed_attachment_count = 0
87 @failed_attachment_count = 0
87 attachments.each do |attachment|
88 attachments.each do |attachment|
88 next unless attachment.is_a?(Hash)
89 next unless attachment.is_a?(Hash)
89 a = nil
90 a = nil
90 if file = attachment['file']
91 if file = attachment['file']
91 next unless file.size > 0
92 next unless file.size > 0
92 a = Attachment.create(:file => file, :author => author)
93 a = Attachment.create(:file => file, :author => author)
93 elsif token = attachment['token']
94 elsif token = attachment['token'].presence
94 a = Attachment.find_by_token(token)
95 a = Attachment.find_by_token(token)
95 unless a
96 unless a
96 @failed_attachment_count += 1
97 @failed_attachment_count += 1
97 next
98 next
98 end
99 end
99 a.filename = attachment['filename'] unless attachment['filename'].blank?
100 a.filename = attachment['filename'] unless attachment['filename'].blank?
100 a.content_type = attachment['content_type'] unless attachment['content_type'].blank?
101 a.content_type = attachment['content_type'] unless attachment['content_type'].blank?
101 end
102 end
102 next unless a
103 next unless a
103 a.description = attachment['description'].to_s.strip
104 a.description = attachment['description'].to_s.strip
104 if a.new_record?
105 if a.new_record?
105 unsaved_attachments << a
106 unsaved_attachments << a
106 else
107 else
107 saved_attachments << a
108 saved_attachments << a
108 end
109 end
109 end
110 end
110 end
111 end
111 {:files => saved_attachments, :unsaved => unsaved_attachments}
112 {:files => saved_attachments, :unsaved => unsaved_attachments}
112 end
113 end
113
114
114 def attach_saved_attachments
115 def attach_saved_attachments
115 saved_attachments.each do |attachment|
116 saved_attachments.each do |attachment|
116 self.attachments << attachment
117 self.attachments << attachment
117 end
118 end
118 end
119 end
119
120
121 def detach_saved_attachments
122 saved_attachments.each do |attachment|
123 # TODO: use #reload instead, after upgrading to Rails 5
124 # (after_rollback is called when running transactional tests in Rails 4)
125 attachment.container = nil
126 end
127 end
128
120 def warn_about_failed_attachments
129 def warn_about_failed_attachments
121 if @failed_attachment_count && @failed_attachment_count > 0
130 if @failed_attachment_count && @failed_attachment_count > 0
122 errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
131 errors.add :base, ::I18n.t('warning_attachments_not_saved', count: @failed_attachment_count)
123 end
132 end
124 end
133 end
125
134
126 module ClassMethods
135 module ClassMethods
127 end
136 end
128 end
137 end
129 end
138 end
130 end
139 end
131 end
140 end
@@ -1,177 +1,168
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 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, lambda {includes(:custom_field).order("#{CustomField.table_name}.position")},
30 has_many :custom_values, lambda {includes(:custom_field).order("#{CustomField.table_name}.position")},
31 :as => :customized,
31 :as => :customized,
32 :dependent => :delete_all,
32 :dependent => :delete_all,
33 :validate => false
33 :validate => false
34
34
35 send :include, Redmine::Acts::Customizable::InstanceMethods
35 send :include, Redmine::Acts::Customizable::InstanceMethods
36 validate :validate_custom_field_values
36 validate :validate_custom_field_values
37 after_save :save_custom_field_values
37 after_save :save_custom_field_values
38 end
38 end
39 end
39 end
40
40
41 module InstanceMethods
41 module InstanceMethods
42 def self.included(base)
42 def self.included(base)
43 base.extend ClassMethods
43 base.extend ClassMethods
44 end
44 end
45
45
46 def available_custom_fields
46 def available_custom_fields
47 CustomField.where("type = '#{self.class.name}CustomField'").sorted.to_a
47 CustomField.where("type = '#{self.class.name}CustomField'").sorted.to_a
48 end
48 end
49
49
50 # Sets the values of the object's custom fields
50 # Sets the values of the object's custom fields
51 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
51 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
52 def custom_fields=(values)
52 def custom_fields=(values)
53 values_to_hash = values.inject({}) do |hash, v|
53 values_to_hash = values.inject({}) do |hash, v|
54 v = v.stringify_keys
54 v = v.stringify_keys
55 if v['id'] && v.has_key?('value')
55 if v['id'] && v.has_key?('value')
56 hash[v['id']] = v['value']
56 hash[v['id']] = v['value']
57 end
57 end
58 hash
58 hash
59 end
59 end
60 self.custom_field_values = values_to_hash
60 self.custom_field_values = values_to_hash
61 end
61 end
62
62
63 # Sets the values of the object's custom fields
63 # Sets the values of the object's custom fields
64 # values is a hash like {'1' => 'foo', 2 => 'bar'}
64 # values is a hash like {'1' => 'foo', 2 => 'bar'}
65 def custom_field_values=(values)
65 def custom_field_values=(values)
66 values = values.stringify_keys
66 values = values.stringify_keys
67
67
68 custom_field_values.each do |custom_field_value|
68 custom_field_values.each do |custom_field_value|
69 key = custom_field_value.custom_field_id.to_s
69 key = custom_field_value.custom_field_id.to_s
70 if values.has_key?(key)
70 if values.has_key?(key)
71 value = values[key]
71 custom_field_value.value = values[key]
72 if value.is_a?(Array)
73 value = value.reject(&:blank?).map(&:to_s).uniq
74 if value.empty?
75 value << ''
76 end
77 else
78 value = value.to_s
79 end
80 custom_field_value.value = value
81 end
72 end
82 end
73 end
83 @custom_field_values_changed = true
74 @custom_field_values_changed = true
84 end
75 end
85
76
86 def custom_field_values
77 def custom_field_values
87 @custom_field_values ||= available_custom_fields.collect do |field|
78 @custom_field_values ||= available_custom_fields.collect do |field|
88 x = CustomFieldValue.new
79 x = CustomFieldValue.new
89 x.custom_field = field
80 x.custom_field = field
90 x.customized = self
81 x.customized = self
91 if field.multiple?
82 if field.multiple?
92 values = custom_values.select { |v| v.custom_field == field }
83 values = custom_values.select { |v| v.custom_field == field }
93 if values.empty?
84 if values.empty?
94 values << custom_values.build(:customized => self, :custom_field => field)
85 values << custom_values.build(:customized => self, :custom_field => field)
95 end
86 end
96 x.value = values.map(&:value)
87 x.instance_variable_set("@value", values.map(&:value))
97 else
88 else
98 cv = custom_values.detect { |v| v.custom_field == field }
89 cv = custom_values.detect { |v| v.custom_field == field }
99 cv ||= custom_values.build(:customized => self, :custom_field => field)
90 cv ||= custom_values.build(:customized => self, :custom_field => field)
100 x.value = cv.value
91 x.instance_variable_set("@value", cv.value)
101 end
92 end
102 x.value_was = x.value.dup if x.value
93 x.value_was = x.value.dup if x.value
103 x
94 x
104 end
95 end
105 end
96 end
106
97
107 def visible_custom_field_values
98 def visible_custom_field_values
108 custom_field_values.select(&:visible?)
99 custom_field_values.select(&:visible?)
109 end
100 end
110
101
111 def custom_field_values_changed?
102 def custom_field_values_changed?
112 @custom_field_values_changed == true
103 @custom_field_values_changed == true
113 end
104 end
114
105
115 def custom_value_for(c)
106 def custom_value_for(c)
116 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
107 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
117 custom_values.detect {|v| v.custom_field_id == field_id }
108 custom_values.detect {|v| v.custom_field_id == field_id }
118 end
109 end
119
110
120 def custom_field_value(c)
111 def custom_field_value(c)
121 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
112 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
122 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
113 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
123 end
114 end
124
115
125 def validate_custom_field_values
116 def validate_custom_field_values
126 if new_record? || custom_field_values_changed?
117 if new_record? || custom_field_values_changed?
127 custom_field_values.each(&:validate_value)
118 custom_field_values.each(&:validate_value)
128 end
119 end
129 end
120 end
130
121
131 def save_custom_field_values
122 def save_custom_field_values
132 target_custom_values = []
123 target_custom_values = []
133 custom_field_values.each do |custom_field_value|
124 custom_field_values.each do |custom_field_value|
134 if custom_field_value.value.is_a?(Array)
125 if custom_field_value.value.is_a?(Array)
135 custom_field_value.value.each do |v|
126 custom_field_value.value.each do |v|
136 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
127 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
137 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
128 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
138 target_custom_values << target
129 target_custom_values << target
139 end
130 end
140 else
131 else
141 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
132 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
142 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
133 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
143 target.value = custom_field_value.value
134 target.value = custom_field_value.value
144 target_custom_values << target
135 target_custom_values << target
145 end
136 end
146 end
137 end
147 self.custom_values = target_custom_values
138 self.custom_values = target_custom_values
148 custom_values.each(&:save)
139 custom_values.each(&:save)
149 @custom_field_values_changed = false
140 @custom_field_values_changed = false
150 true
141 true
151 end
142 end
152
143
153 def reassign_custom_field_values
144 def reassign_custom_field_values
154 if @custom_field_values
145 if @custom_field_values
155 values = @custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
146 values = @custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
156 @custom_field_values = nil
147 @custom_field_values = nil
157 self.custom_field_values = values
148 self.custom_field_values = values
158 end
149 end
159 end
150 end
160
151
161 def reset_custom_values!
152 def reset_custom_values!
162 @custom_field_values = nil
153 @custom_field_values = nil
163 @custom_field_values_changed = true
154 @custom_field_values_changed = true
164 end
155 end
165
156
166 def reload(*args)
157 def reload(*args)
167 @custom_field_values = nil
158 @custom_field_values = nil
168 @custom_field_values_changed = false
159 @custom_field_values_changed = false
169 super
160 super
170 end
161 end
171
162
172 module ClassMethods
163 module ClassMethods
173 end
164 end
174 end
165 end
175 end
166 end
176 end
167 end
177 end
168 end
@@ -1,834 +1,963
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 'uri'
18 require 'uri'
19
19
20 module Redmine
20 module Redmine
21 module FieldFormat
21 module FieldFormat
22 def self.add(name, klass)
22 def self.add(name, klass)
23 all[name.to_s] = klass.instance
23 all[name.to_s] = klass.instance
24 end
24 end
25
25
26 def self.delete(name)
26 def self.delete(name)
27 all.delete(name.to_s)
27 all.delete(name.to_s)
28 end
28 end
29
29
30 def self.all
30 def self.all
31 @formats ||= Hash.new(Base.instance)
31 @formats ||= Hash.new(Base.instance)
32 end
32 end
33
33
34 def self.available_formats
34 def self.available_formats
35 all.keys
35 all.keys
36 end
36 end
37
37
38 def self.find(name)
38 def self.find(name)
39 all[name.to_s]
39 all[name.to_s]
40 end
40 end
41
41
42 # Return an array of custom field formats which can be used in select_tag
42 # Return an array of custom field formats which can be used in select_tag
43 def self.as_select(class_name=nil)
43 def self.as_select(class_name=nil)
44 formats = all.values.select do |format|
44 formats = all.values.select do |format|
45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
46 end
46 end
47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
48 end
48 end
49
49
50 # Returns an array of formats that can be used for a custom field class
50 # Returns an array of formats that can be used for a custom field class
51 def self.formats_for_custom_field_class(klass=nil)
51 def self.formats_for_custom_field_class(klass=nil)
52 all.values.select do |format|
52 all.values.select do |format|
53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
54 end
54 end
55 end
55 end
56
56
57 class Base
57 class Base
58 include Singleton
58 include Singleton
59 include Redmine::I18n
59 include Redmine::I18n
60 include Redmine::Helpers::URL
60 include Redmine::Helpers::URL
61 include ERB::Util
61 include ERB::Util
62
62
63 class_attribute :format_name
63 class_attribute :format_name
64 self.format_name = nil
64 self.format_name = nil
65
65
66 # Set this to true if the format supports multiple values
66 # Set this to true if the format supports multiple values
67 class_attribute :multiple_supported
67 class_attribute :multiple_supported
68 self.multiple_supported = false
68 self.multiple_supported = false
69
69
70 # Set this to true if the format supports filtering on custom values
71 class_attribute :is_filter_supported
72 self.is_filter_supported = true
73
70 # Set this to true if the format supports textual search on custom values
74 # Set this to true if the format supports textual search on custom values
71 class_attribute :searchable_supported
75 class_attribute :searchable_supported
72 self.searchable_supported = false
76 self.searchable_supported = false
73
77
74 # Set this to true if field values can be summed up
78 # Set this to true if field values can be summed up
75 class_attribute :totalable_supported
79 class_attribute :totalable_supported
76 self.totalable_supported = false
80 self.totalable_supported = false
77
81
78 # Restricts the classes that the custom field can be added to
82 # Restricts the classes that the custom field can be added to
79 # Set to nil for no restrictions
83 # Set to nil for no restrictions
80 class_attribute :customized_class_names
84 class_attribute :customized_class_names
81 self.customized_class_names = nil
85 self.customized_class_names = nil
82
86
83 # Name of the partial for editing the custom field
87 # Name of the partial for editing the custom field
84 class_attribute :form_partial
88 class_attribute :form_partial
85 self.form_partial = nil
89 self.form_partial = nil
86
90
87 class_attribute :change_as_diff
91 class_attribute :change_as_diff
88 self.change_as_diff = false
92 self.change_as_diff = false
89
93
94 class_attribute :change_no_details
95 self.change_no_details = false
96
90 def self.add(name)
97 def self.add(name)
91 self.format_name = name
98 self.format_name = name
92 Redmine::FieldFormat.add(name, self)
99 Redmine::FieldFormat.add(name, self)
93 end
100 end
94 private_class_method :add
101 private_class_method :add
95
102
96 def self.field_attributes(*args)
103 def self.field_attributes(*args)
97 CustomField.store_accessor :format_store, *args
104 CustomField.store_accessor :format_store, *args
98 end
105 end
99
106
100 field_attributes :url_pattern
107 field_attributes :url_pattern
101
108
102 def name
109 def name
103 self.class.format_name
110 self.class.format_name
104 end
111 end
105
112
106 def label
113 def label
107 "label_#{name}"
114 "label_#{name}"
108 end
115 end
109
116
117 def set_custom_field_value(custom_field, custom_field_value, value)
118 if value.is_a?(Array)
119 value = value.map(&:to_s).reject{|v| v==''}.uniq
120 if value.empty?
121 value << ''
122 end
123 else
124 value = value.to_s
125 end
126
127 value
128 end
129
110 def cast_custom_value(custom_value)
130 def cast_custom_value(custom_value)
111 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
131 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
112 end
132 end
113
133
114 def cast_value(custom_field, value, customized=nil)
134 def cast_value(custom_field, value, customized=nil)
115 if value.blank?
135 if value.blank?
116 nil
136 nil
117 elsif value.is_a?(Array)
137 elsif value.is_a?(Array)
118 casted = value.map do |v|
138 casted = value.map do |v|
119 cast_single_value(custom_field, v, customized)
139 cast_single_value(custom_field, v, customized)
120 end
140 end
121 casted.compact.sort
141 casted.compact.sort
122 else
142 else
123 cast_single_value(custom_field, value, customized)
143 cast_single_value(custom_field, value, customized)
124 end
144 end
125 end
145 end
126
146
127 def cast_single_value(custom_field, value, customized=nil)
147 def cast_single_value(custom_field, value, customized=nil)
128 value.to_s
148 value.to_s
129 end
149 end
130
150
131 def target_class
151 def target_class
132 nil
152 nil
133 end
153 end
134
154
135 def possible_custom_value_options(custom_value)
155 def possible_custom_value_options(custom_value)
136 possible_values_options(custom_value.custom_field, custom_value.customized)
156 possible_values_options(custom_value.custom_field, custom_value.customized)
137 end
157 end
138
158
139 def possible_values_options(custom_field, object=nil)
159 def possible_values_options(custom_field, object=nil)
140 []
160 []
141 end
161 end
142
162
143 def value_from_keyword(custom_field, keyword, object)
163 def value_from_keyword(custom_field, keyword, object)
144 possible_values_options = possible_values_options(custom_field, object)
164 possible_values_options = possible_values_options(custom_field, object)
145 if possible_values_options.present?
165 if possible_values_options.present?
146 keyword = keyword.to_s
166 keyword = keyword.to_s
147 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
167 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
148 if v.is_a?(Array)
168 if v.is_a?(Array)
149 v.last
169 v.last
150 else
170 else
151 v
171 v
152 end
172 end
153 end
173 end
154 else
174 else
155 keyword
175 keyword
156 end
176 end
157 end
177 end
158
178
159 # Returns the validation errors for custom_field
179 # Returns the validation errors for custom_field
160 # Should return an empty array if custom_field is valid
180 # Should return an empty array if custom_field is valid
161 def validate_custom_field(custom_field)
181 def validate_custom_field(custom_field)
162 errors = []
182 errors = []
163 pattern = custom_field.url_pattern
183 pattern = custom_field.url_pattern
164 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
184 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
165 errors << [:url_pattern, :invalid]
185 errors << [:url_pattern, :invalid]
166 end
186 end
167 errors
187 errors
168 end
188 end
169
189
170 # Returns the validation error messages for custom_value
190 # Returns the validation error messages for custom_value
171 # Should return an empty array if custom_value is valid
191 # Should return an empty array if custom_value is valid
192 # custom_value is a CustomFieldValue.
172 def validate_custom_value(custom_value)
193 def validate_custom_value(custom_value)
173 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
194 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
174 errors = values.map do |value|
195 errors = values.map do |value|
175 validate_single_value(custom_value.custom_field, value, custom_value.customized)
196 validate_single_value(custom_value.custom_field, value, custom_value.customized)
176 end
197 end
177 errors.flatten.uniq
198 errors.flatten.uniq
178 end
199 end
179
200
180 def validate_single_value(custom_field, value, customized=nil)
201 def validate_single_value(custom_field, value, customized=nil)
181 []
202 []
182 end
203 end
183
204
205 # CustomValue after_save callback
206 def after_save_custom_value(custom_field, custom_value)
207 end
208
184 def formatted_custom_value(view, custom_value, html=false)
209 def formatted_custom_value(view, custom_value, html=false)
185 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
210 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
186 end
211 end
187
212
188 def formatted_value(view, custom_field, value, customized=nil, html=false)
213 def formatted_value(view, custom_field, value, customized=nil, html=false)
189 casted = cast_value(custom_field, value, customized)
214 casted = cast_value(custom_field, value, customized)
190 if html && custom_field.url_pattern.present?
215 if html && custom_field.url_pattern.present?
191 texts_and_urls = Array.wrap(casted).map do |single_value|
216 texts_and_urls = Array.wrap(casted).map do |single_value|
192 text = view.format_object(single_value, false).to_s
217 text = view.format_object(single_value, false).to_s
193 url = url_from_pattern(custom_field, single_value, customized)
218 url = url_from_pattern(custom_field, single_value, customized)
194 [text, url]
219 [text, url]
195 end
220 end
196 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
221 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
197 links.join(', ').html_safe
222 links.join(', ').html_safe
198 else
223 else
199 casted
224 casted
200 end
225 end
201 end
226 end
202
227
203 # Returns an URL generated with the custom field URL pattern
228 # Returns an URL generated with the custom field URL pattern
204 # and variables substitution:
229 # and variables substitution:
205 # %value% => the custom field value
230 # %value% => the custom field value
206 # %id% => id of the customized object
231 # %id% => id of the customized object
207 # %project_id% => id of the project of the customized object if defined
232 # %project_id% => id of the project of the customized object if defined
208 # %project_identifier% => identifier of the project of the customized object if defined
233 # %project_identifier% => identifier of the project of the customized object if defined
209 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
234 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
210 def url_from_pattern(custom_field, value, customized)
235 def url_from_pattern(custom_field, value, customized)
211 url = custom_field.url_pattern.to_s.dup
236 url = custom_field.url_pattern.to_s.dup
212 url.gsub!('%value%') {URI.encode value.to_s}
237 url.gsub!('%value%') {URI.encode value.to_s}
213 url.gsub!('%id%') {URI.encode customized.id.to_s}
238 url.gsub!('%id%') {URI.encode customized.id.to_s}
214 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
239 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
215 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
240 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
216 if custom_field.regexp.present?
241 if custom_field.regexp.present?
217 url.gsub!(%r{%m(\d+)%}) do
242 url.gsub!(%r{%m(\d+)%}) do
218 m = $1.to_i
243 m = $1.to_i
219 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
244 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
220 URI.encode matches[m].to_s
245 URI.encode matches[m].to_s
221 end
246 end
222 end
247 end
223 end
248 end
224 url
249 url
225 end
250 end
226 protected :url_from_pattern
251 protected :url_from_pattern
227
252
228 # Returns the URL pattern with substitution tokens removed,
253 # Returns the URL pattern with substitution tokens removed,
229 # for validation purpose
254 # for validation purpose
230 def url_pattern_without_tokens(url_pattern)
255 def url_pattern_without_tokens(url_pattern)
231 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
256 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
232 end
257 end
233 protected :url_pattern_without_tokens
258 protected :url_pattern_without_tokens
234
259
235 def edit_tag(view, tag_id, tag_name, custom_value, options={})
260 def edit_tag(view, tag_id, tag_name, custom_value, options={})
236 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
261 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
237 end
262 end
238
263
239 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
264 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
240 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
265 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
241 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
266 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
242 end
267 end
243
268
244 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
269 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
245 if custom_field.is_required?
270 if custom_field.is_required?
246 ''.html_safe
271 ''.html_safe
247 else
272 else
248 view.content_tag('label',
273 view.content_tag('label',
249 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
274 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
250 :class => 'inline'
275 :class => 'inline'
251 )
276 )
252 end
277 end
253 end
278 end
254 protected :bulk_clear_tag
279 protected :bulk_clear_tag
255
280
256 def query_filter_options(custom_field, query)
281 def query_filter_options(custom_field, query)
257 {:type => :string}
282 {:type => :string}
258 end
283 end
259
284
260 def before_custom_field_save(custom_field)
285 def before_custom_field_save(custom_field)
261 end
286 end
262
287
263 # Returns a ORDER BY clause that can used to sort customized
288 # Returns a ORDER BY clause that can used to sort customized
264 # objects by their value of the custom field.
289 # objects by their value of the custom field.
265 # Returns nil if the custom field can not be used for sorting.
290 # Returns nil if the custom field can not be used for sorting.
266 def order_statement(custom_field)
291 def order_statement(custom_field)
267 # COALESCE is here to make sure that blank and NULL values are sorted equally
292 # COALESCE is here to make sure that blank and NULL values are sorted equally
268 "COALESCE(#{join_alias custom_field}.value, '')"
293 "COALESCE(#{join_alias custom_field}.value, '')"
269 end
294 end
270
295
271 # Returns a GROUP BY clause that can used to group by custom value
296 # Returns a GROUP BY clause that can used to group by custom value
272 # Returns nil if the custom field can not be used for grouping.
297 # Returns nil if the custom field can not be used for grouping.
273 def group_statement(custom_field)
298 def group_statement(custom_field)
274 nil
299 nil
275 end
300 end
276
301
277 # Returns a JOIN clause that is added to the query when sorting by custom values
302 # Returns a JOIN clause that is added to the query when sorting by custom values
278 def join_for_order_statement(custom_field)
303 def join_for_order_statement(custom_field)
279 alias_name = join_alias(custom_field)
304 alias_name = join_alias(custom_field)
280
305
281 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
306 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
282 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
307 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
283 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
308 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
284 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
309 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
285 " AND (#{custom_field.visibility_by_project_condition})" +
310 " AND (#{custom_field.visibility_by_project_condition})" +
286 " AND #{alias_name}.value <> ''" +
311 " AND #{alias_name}.value <> ''" +
287 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
312 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
288 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
313 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
289 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
314 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
290 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
315 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
291 end
316 end
292
317
293 def join_alias(custom_field)
318 def join_alias(custom_field)
294 "cf_#{custom_field.id}"
319 "cf_#{custom_field.id}"
295 end
320 end
296 protected :join_alias
321 protected :join_alias
297 end
322 end
298
323
299 class Unbounded < Base
324 class Unbounded < Base
300 def validate_single_value(custom_field, value, customized=nil)
325 def validate_single_value(custom_field, value, customized=nil)
301 errs = super
326 errs = super
302 value = value.to_s
327 value = value.to_s
303 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
328 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
304 errs << ::I18n.t('activerecord.errors.messages.invalid')
329 errs << ::I18n.t('activerecord.errors.messages.invalid')
305 end
330 end
306 if custom_field.min_length && value.length < custom_field.min_length
331 if custom_field.min_length && value.length < custom_field.min_length
307 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
332 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
308 end
333 end
309 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
334 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
310 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
335 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
311 end
336 end
312 errs
337 errs
313 end
338 end
314 end
339 end
315
340
316 class StringFormat < Unbounded
341 class StringFormat < Unbounded
317 add 'string'
342 add 'string'
318 self.searchable_supported = true
343 self.searchable_supported = true
319 self.form_partial = 'custom_fields/formats/string'
344 self.form_partial = 'custom_fields/formats/string'
320 field_attributes :text_formatting
345 field_attributes :text_formatting
321
346
322 def formatted_value(view, custom_field, value, customized=nil, html=false)
347 def formatted_value(view, custom_field, value, customized=nil, html=false)
323 if html
348 if html
324 if custom_field.url_pattern.present?
349 if custom_field.url_pattern.present?
325 super
350 super
326 elsif custom_field.text_formatting == 'full'
351 elsif custom_field.text_formatting == 'full'
327 view.textilizable(value, :object => customized)
352 view.textilizable(value, :object => customized)
328 else
353 else
329 value.to_s
354 value.to_s
330 end
355 end
331 else
356 else
332 value.to_s
357 value.to_s
333 end
358 end
334 end
359 end
335 end
360 end
336
361
337 class TextFormat < Unbounded
362 class TextFormat < Unbounded
338 add 'text'
363 add 'text'
339 self.searchable_supported = true
364 self.searchable_supported = true
340 self.form_partial = 'custom_fields/formats/text'
365 self.form_partial = 'custom_fields/formats/text'
341 self.change_as_diff = true
366 self.change_as_diff = true
342
367
343 def formatted_value(view, custom_field, value, customized=nil, html=false)
368 def formatted_value(view, custom_field, value, customized=nil, html=false)
344 if html
369 if html
345 if value.present?
370 if value.present?
346 if custom_field.text_formatting == 'full'
371 if custom_field.text_formatting == 'full'
347 view.textilizable(value, :object => customized)
372 view.textilizable(value, :object => customized)
348 else
373 else
349 view.simple_format(html_escape(value))
374 view.simple_format(html_escape(value))
350 end
375 end
351 else
376 else
352 ''
377 ''
353 end
378 end
354 else
379 else
355 value.to_s
380 value.to_s
356 end
381 end
357 end
382 end
358
383
359 def edit_tag(view, tag_id, tag_name, custom_value, options={})
384 def edit_tag(view, tag_id, tag_name, custom_value, options={})
360 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
385 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
361 end
386 end
362
387
363 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
388 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
364 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
389 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
365 '<br />'.html_safe +
390 '<br />'.html_safe +
366 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
391 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
367 end
392 end
368
393
369 def query_filter_options(custom_field, query)
394 def query_filter_options(custom_field, query)
370 {:type => :text}
395 {:type => :text}
371 end
396 end
372 end
397 end
373
398
374 class LinkFormat < StringFormat
399 class LinkFormat < StringFormat
375 add 'link'
400 add 'link'
376 self.searchable_supported = false
401 self.searchable_supported = false
377 self.form_partial = 'custom_fields/formats/link'
402 self.form_partial = 'custom_fields/formats/link'
378
403
379 def formatted_value(view, custom_field, value, customized=nil, html=false)
404 def formatted_value(view, custom_field, value, customized=nil, html=false)
380 if html && value.present?
405 if html && value.present?
381 if custom_field.url_pattern.present?
406 if custom_field.url_pattern.present?
382 url = url_from_pattern(custom_field, value, customized)
407 url = url_from_pattern(custom_field, value, customized)
383 else
408 else
384 url = value.to_s
409 url = value.to_s
385 unless url =~ %r{\A[a-z]+://}i
410 unless url =~ %r{\A[a-z]+://}i
386 # no protocol found, use http by default
411 # no protocol found, use http by default
387 url = "http://" + url
412 url = "http://" + url
388 end
413 end
389 end
414 end
390 view.link_to value.to_s.truncate(40), url
415 view.link_to value.to_s.truncate(40), url
391 else
416 else
392 value.to_s
417 value.to_s
393 end
418 end
394 end
419 end
395 end
420 end
396
421
397 class Numeric < Unbounded
422 class Numeric < Unbounded
398 self.form_partial = 'custom_fields/formats/numeric'
423 self.form_partial = 'custom_fields/formats/numeric'
399 self.totalable_supported = true
424 self.totalable_supported = true
400
425
401 def order_statement(custom_field)
426 def order_statement(custom_field)
402 # Make the database cast values into numeric
427 # Make the database cast values into numeric
403 # Postgresql will raise an error if a value can not be casted!
428 # Postgresql will raise an error if a value can not be casted!
404 # CustomValue validations should ensure that it doesn't occur
429 # CustomValue validations should ensure that it doesn't occur
405 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
430 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
406 end
431 end
407
432
408 # Returns totals for the given scope
433 # Returns totals for the given scope
409 def total_for_scope(custom_field, scope)
434 def total_for_scope(custom_field, scope)
410 scope.joins(:custom_values).
435 scope.joins(:custom_values).
411 where(:custom_values => {:custom_field_id => custom_field.id}).
436 where(:custom_values => {:custom_field_id => custom_field.id}).
412 where.not(:custom_values => {:value => ''}).
437 where.not(:custom_values => {:value => ''}).
413 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
438 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
414 end
439 end
415
440
416 def cast_total_value(custom_field, value)
441 def cast_total_value(custom_field, value)
417 cast_single_value(custom_field, value)
442 cast_single_value(custom_field, value)
418 end
443 end
419 end
444 end
420
445
421 class IntFormat < Numeric
446 class IntFormat < Numeric
422 add 'int'
447 add 'int'
423
448
424 def label
449 def label
425 "label_integer"
450 "label_integer"
426 end
451 end
427
452
428 def cast_single_value(custom_field, value, customized=nil)
453 def cast_single_value(custom_field, value, customized=nil)
429 value.to_i
454 value.to_i
430 end
455 end
431
456
432 def validate_single_value(custom_field, value, customized=nil)
457 def validate_single_value(custom_field, value, customized=nil)
433 errs = super
458 errs = super
434 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
459 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
435 errs
460 errs
436 end
461 end
437
462
438 def query_filter_options(custom_field, query)
463 def query_filter_options(custom_field, query)
439 {:type => :integer}
464 {:type => :integer}
440 end
465 end
441
466
442 def group_statement(custom_field)
467 def group_statement(custom_field)
443 order_statement(custom_field)
468 order_statement(custom_field)
444 end
469 end
445 end
470 end
446
471
447 class FloatFormat < Numeric
472 class FloatFormat < Numeric
448 add 'float'
473 add 'float'
449
474
450 def cast_single_value(custom_field, value, customized=nil)
475 def cast_single_value(custom_field, value, customized=nil)
451 value.to_f
476 value.to_f
452 end
477 end
453
478
454 def cast_total_value(custom_field, value)
479 def cast_total_value(custom_field, value)
455 value.to_f.round(2)
480 value.to_f.round(2)
456 end
481 end
457
482
458 def validate_single_value(custom_field, value, customized=nil)
483 def validate_single_value(custom_field, value, customized=nil)
459 errs = super
484 errs = super
460 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
485 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
461 errs
486 errs
462 end
487 end
463
488
464 def query_filter_options(custom_field, query)
489 def query_filter_options(custom_field, query)
465 {:type => :float}
490 {:type => :float}
466 end
491 end
467 end
492 end
468
493
469 class DateFormat < Unbounded
494 class DateFormat < Unbounded
470 add 'date'
495 add 'date'
471 self.form_partial = 'custom_fields/formats/date'
496 self.form_partial = 'custom_fields/formats/date'
472
497
473 def cast_single_value(custom_field, value, customized=nil)
498 def cast_single_value(custom_field, value, customized=nil)
474 value.to_date rescue nil
499 value.to_date rescue nil
475 end
500 end
476
501
477 def validate_single_value(custom_field, value, customized=nil)
502 def validate_single_value(custom_field, value, customized=nil)
478 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
503 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
479 []
504 []
480 else
505 else
481 [::I18n.t('activerecord.errors.messages.not_a_date')]
506 [::I18n.t('activerecord.errors.messages.not_a_date')]
482 end
507 end
483 end
508 end
484
509
485 def edit_tag(view, tag_id, tag_name, custom_value, options={})
510 def edit_tag(view, tag_id, tag_name, custom_value, options={})
486 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
511 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
487 view.calendar_for(tag_id)
512 view.calendar_for(tag_id)
488 end
513 end
489
514
490 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
515 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
491 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
516 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
492 view.calendar_for(tag_id) +
517 view.calendar_for(tag_id) +
493 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
518 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
494 end
519 end
495
520
496 def query_filter_options(custom_field, query)
521 def query_filter_options(custom_field, query)
497 {:type => :date}
522 {:type => :date}
498 end
523 end
499
524
500 def group_statement(custom_field)
525 def group_statement(custom_field)
501 order_statement(custom_field)
526 order_statement(custom_field)
502 end
527 end
503 end
528 end
504
529
505 class List < Base
530 class List < Base
506 self.multiple_supported = true
531 self.multiple_supported = true
507 field_attributes :edit_tag_style
532 field_attributes :edit_tag_style
508
533
509 def edit_tag(view, tag_id, tag_name, custom_value, options={})
534 def edit_tag(view, tag_id, tag_name, custom_value, options={})
510 if custom_value.custom_field.edit_tag_style == 'check_box'
535 if custom_value.custom_field.edit_tag_style == 'check_box'
511 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
536 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
512 else
537 else
513 select_edit_tag(view, tag_id, tag_name, custom_value, options)
538 select_edit_tag(view, tag_id, tag_name, custom_value, options)
514 end
539 end
515 end
540 end
516
541
517 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
542 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
518 opts = []
543 opts = []
519 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
544 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
520 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
545 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
521 opts += possible_values_options(custom_field, objects)
546 opts += possible_values_options(custom_field, objects)
522 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
547 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
523 end
548 end
524
549
525 def query_filter_options(custom_field, query)
550 def query_filter_options(custom_field, query)
526 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
551 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
527 end
552 end
528
553
529 protected
554 protected
530
555
531 # Returns the values that are available in the field filter
556 # Returns the values that are available in the field filter
532 def query_filter_values(custom_field, query)
557 def query_filter_values(custom_field, query)
533 possible_values_options(custom_field, query.project)
558 possible_values_options(custom_field, query.project)
534 end
559 end
535
560
536 # Renders the edit tag as a select tag
561 # Renders the edit tag as a select tag
537 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
562 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
538 blank_option = ''.html_safe
563 blank_option = ''.html_safe
539 unless custom_value.custom_field.multiple?
564 unless custom_value.custom_field.multiple?
540 if custom_value.custom_field.is_required?
565 if custom_value.custom_field.is_required?
541 unless custom_value.custom_field.default_value.present?
566 unless custom_value.custom_field.default_value.present?
542 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
567 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
543 end
568 end
544 else
569 else
545 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
570 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
546 end
571 end
547 end
572 end
548 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
573 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
549 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
574 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
550 if custom_value.custom_field.multiple?
575 if custom_value.custom_field.multiple?
551 s << view.hidden_field_tag(tag_name, '')
576 s << view.hidden_field_tag(tag_name, '')
552 end
577 end
553 s
578 s
554 end
579 end
555
580
556 # Renders the edit tag as check box or radio tags
581 # Renders the edit tag as check box or radio tags
557 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
582 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
558 opts = []
583 opts = []
559 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
584 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
560 opts << ["(#{l(:label_none)})", '']
585 opts << ["(#{l(:label_none)})", '']
561 end
586 end
562 opts += possible_custom_value_options(custom_value)
587 opts += possible_custom_value_options(custom_value)
563 s = ''.html_safe
588 s = ''.html_safe
564 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
589 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
565 opts.each do |label, value|
590 opts.each do |label, value|
566 value ||= label
591 value ||= label
567 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
592 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
568 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
593 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
569 # set the id on the first tag only
594 # set the id on the first tag only
570 tag_id = nil
595 tag_id = nil
571 s << view.content_tag('label', tag + ' ' + label)
596 s << view.content_tag('label', tag + ' ' + label)
572 end
597 end
573 if custom_value.custom_field.multiple?
598 if custom_value.custom_field.multiple?
574 s << view.hidden_field_tag(tag_name, '')
599 s << view.hidden_field_tag(tag_name, '')
575 end
600 end
576 css = "#{options[:class]} check_box_group"
601 css = "#{options[:class]} check_box_group"
577 view.content_tag('span', s, options.merge(:class => css))
602 view.content_tag('span', s, options.merge(:class => css))
578 end
603 end
579 end
604 end
580
605
581 class ListFormat < List
606 class ListFormat < List
582 add 'list'
607 add 'list'
583 self.searchable_supported = true
608 self.searchable_supported = true
584 self.form_partial = 'custom_fields/formats/list'
609 self.form_partial = 'custom_fields/formats/list'
585
610
586 def possible_custom_value_options(custom_value)
611 def possible_custom_value_options(custom_value)
587 options = possible_values_options(custom_value.custom_field)
612 options = possible_values_options(custom_value.custom_field)
588 missing = [custom_value.value].flatten.reject(&:blank?) - options
613 missing = [custom_value.value].flatten.reject(&:blank?) - options
589 if missing.any?
614 if missing.any?
590 options += missing
615 options += missing
591 end
616 end
592 options
617 options
593 end
618 end
594
619
595 def possible_values_options(custom_field, object=nil)
620 def possible_values_options(custom_field, object=nil)
596 custom_field.possible_values
621 custom_field.possible_values
597 end
622 end
598
623
599 def validate_custom_field(custom_field)
624 def validate_custom_field(custom_field)
600 errors = []
625 errors = []
601 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
626 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
602 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
627 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
603 errors
628 errors
604 end
629 end
605
630
606 def validate_custom_value(custom_value)
631 def validate_custom_value(custom_value)
607 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
632 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
608 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
633 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
609 if invalid_values.any?
634 if invalid_values.any?
610 [::I18n.t('activerecord.errors.messages.inclusion')]
635 [::I18n.t('activerecord.errors.messages.inclusion')]
611 else
636 else
612 []
637 []
613 end
638 end
614 end
639 end
615
640
616 def group_statement(custom_field)
641 def group_statement(custom_field)
617 order_statement(custom_field)
642 order_statement(custom_field)
618 end
643 end
619 end
644 end
620
645
621 class BoolFormat < List
646 class BoolFormat < List
622 add 'bool'
647 add 'bool'
623 self.multiple_supported = false
648 self.multiple_supported = false
624 self.form_partial = 'custom_fields/formats/bool'
649 self.form_partial = 'custom_fields/formats/bool'
625
650
626 def label
651 def label
627 "label_boolean"
652 "label_boolean"
628 end
653 end
629
654
630 def cast_single_value(custom_field, value, customized=nil)
655 def cast_single_value(custom_field, value, customized=nil)
631 value == '1' ? true : false
656 value == '1' ? true : false
632 end
657 end
633
658
634 def possible_values_options(custom_field, object=nil)
659 def possible_values_options(custom_field, object=nil)
635 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
660 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
636 end
661 end
637
662
638 def group_statement(custom_field)
663 def group_statement(custom_field)
639 order_statement(custom_field)
664 order_statement(custom_field)
640 end
665 end
641
666
642 def edit_tag(view, tag_id, tag_name, custom_value, options={})
667 def edit_tag(view, tag_id, tag_name, custom_value, options={})
643 case custom_value.custom_field.edit_tag_style
668 case custom_value.custom_field.edit_tag_style
644 when 'check_box'
669 when 'check_box'
645 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
670 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
646 when 'radio'
671 when 'radio'
647 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
672 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
648 else
673 else
649 select_edit_tag(view, tag_id, tag_name, custom_value, options)
674 select_edit_tag(view, tag_id, tag_name, custom_value, options)
650 end
675 end
651 end
676 end
652
677
653 # Renders the edit tag as a simple check box
678 # Renders the edit tag as a simple check box
654 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
679 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
655 s = ''.html_safe
680 s = ''.html_safe
656 s << view.hidden_field_tag(tag_name, '0', :id => nil)
681 s << view.hidden_field_tag(tag_name, '0', :id => nil)
657 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
682 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
658 view.content_tag('span', s, options)
683 view.content_tag('span', s, options)
659 end
684 end
660 end
685 end
661
686
662 class RecordList < List
687 class RecordList < List
663 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
688 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
664
689
665 def cast_single_value(custom_field, value, customized=nil)
690 def cast_single_value(custom_field, value, customized=nil)
666 target_class.find_by_id(value.to_i) if value.present?
691 target_class.find_by_id(value.to_i) if value.present?
667 end
692 end
668
693
669 def target_class
694 def target_class
670 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
695 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
671 end
696 end
672
697
673 def reset_target_class
698 def reset_target_class
674 @target_class = nil
699 @target_class = nil
675 end
700 end
676
701
677 def possible_custom_value_options(custom_value)
702 def possible_custom_value_options(custom_value)
678 options = possible_values_options(custom_value.custom_field, custom_value.customized)
703 options = possible_values_options(custom_value.custom_field, custom_value.customized)
679 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
704 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
680 if missing.any?
705 if missing.any?
681 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
706 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
682 end
707 end
683 options
708 options
684 end
709 end
685
710
686 def order_statement(custom_field)
711 def order_statement(custom_field)
687 if target_class.respond_to?(:fields_for_order_statement)
712 if target_class.respond_to?(:fields_for_order_statement)
688 target_class.fields_for_order_statement(value_join_alias(custom_field))
713 target_class.fields_for_order_statement(value_join_alias(custom_field))
689 end
714 end
690 end
715 end
691
716
692 def group_statement(custom_field)
717 def group_statement(custom_field)
693 "COALESCE(#{join_alias custom_field}.value, '')"
718 "COALESCE(#{join_alias custom_field}.value, '')"
694 end
719 end
695
720
696 def join_for_order_statement(custom_field)
721 def join_for_order_statement(custom_field)
697 alias_name = join_alias(custom_field)
722 alias_name = join_alias(custom_field)
698
723
699 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
724 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
700 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
725 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
701 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
726 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
702 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
727 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
703 " AND (#{custom_field.visibility_by_project_condition})" +
728 " AND (#{custom_field.visibility_by_project_condition})" +
704 " AND #{alias_name}.value <> ''" +
729 " AND #{alias_name}.value <> ''" +
705 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
730 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
706 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
731 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
707 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
732 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
708 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
733 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
709 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
734 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
710 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
735 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
711 end
736 end
712
737
713 def value_join_alias(custom_field)
738 def value_join_alias(custom_field)
714 join_alias(custom_field) + "_" + custom_field.field_format
739 join_alias(custom_field) + "_" + custom_field.field_format
715 end
740 end
716 protected :value_join_alias
741 protected :value_join_alias
717 end
742 end
718
743
719 class EnumerationFormat < RecordList
744 class EnumerationFormat < RecordList
720 add 'enumeration'
745 add 'enumeration'
721 self.form_partial = 'custom_fields/formats/enumeration'
746 self.form_partial = 'custom_fields/formats/enumeration'
722
747
723 def label
748 def label
724 "label_field_format_enumeration"
749 "label_field_format_enumeration"
725 end
750 end
726
751
727 def target_class
752 def target_class
728 @target_class ||= CustomFieldEnumeration
753 @target_class ||= CustomFieldEnumeration
729 end
754 end
730
755
731 def possible_values_options(custom_field, object=nil)
756 def possible_values_options(custom_field, object=nil)
732 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
757 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
733 end
758 end
734
759
735 def possible_values_records(custom_field, object=nil)
760 def possible_values_records(custom_field, object=nil)
736 custom_field.enumerations.active
761 custom_field.enumerations.active
737 end
762 end
738
763
739 def value_from_keyword(custom_field, keyword, object)
764 def value_from_keyword(custom_field, keyword, object)
740 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
765 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
741 value ? value.id : nil
766 value ? value.id : nil
742 end
767 end
743 end
768 end
744
769
745 class UserFormat < RecordList
770 class UserFormat < RecordList
746 add 'user'
771 add 'user'
747 self.form_partial = 'custom_fields/formats/user'
772 self.form_partial = 'custom_fields/formats/user'
748 field_attributes :user_role
773 field_attributes :user_role
749
774
750 def possible_values_options(custom_field, object=nil)
775 def possible_values_options(custom_field, object=nil)
751 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
776 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
752 end
777 end
753
778
754 def possible_values_records(custom_field, object=nil)
779 def possible_values_records(custom_field, object=nil)
755 if object.is_a?(Array)
780 if object.is_a?(Array)
756 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
781 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
757 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
782 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
758 elsif object.respond_to?(:project) && object.project
783 elsif object.respond_to?(:project) && object.project
759 scope = object.project.users
784 scope = object.project.users
760 if custom_field.user_role.is_a?(Array)
785 if custom_field.user_role.is_a?(Array)
761 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
786 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
762 if role_ids.any?
787 if role_ids.any?
763 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
788 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
764 end
789 end
765 end
790 end
766 scope.sorted
791 scope.sorted
767 else
792 else
768 []
793 []
769 end
794 end
770 end
795 end
771
796
772 def value_from_keyword(custom_field, keyword, object)
797 def value_from_keyword(custom_field, keyword, object)
773 users = possible_values_records(custom_field, object).to_a
798 users = possible_values_records(custom_field, object).to_a
774 user = Principal.detect_by_keyword(users, keyword)
799 user = Principal.detect_by_keyword(users, keyword)
775 user ? user.id : nil
800 user ? user.id : nil
776 end
801 end
777
802
778 def before_custom_field_save(custom_field)
803 def before_custom_field_save(custom_field)
779 super
804 super
780 if custom_field.user_role.is_a?(Array)
805 if custom_field.user_role.is_a?(Array)
781 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
806 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
782 end
807 end
783 end
808 end
784 end
809 end
785
810
786 class VersionFormat < RecordList
811 class VersionFormat < RecordList
787 add 'version'
812 add 'version'
788 self.form_partial = 'custom_fields/formats/version'
813 self.form_partial = 'custom_fields/formats/version'
789 field_attributes :version_status
814 field_attributes :version_status
790
815
791 def possible_values_options(custom_field, object=nil)
816 def possible_values_options(custom_field, object=nil)
792 versions_options(custom_field, object)
817 versions_options(custom_field, object)
793 end
818 end
794
819
795 def before_custom_field_save(custom_field)
820 def before_custom_field_save(custom_field)
796 super
821 super
797 if custom_field.version_status.is_a?(Array)
822 if custom_field.version_status.is_a?(Array)
798 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
823 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
799 end
824 end
800 end
825 end
801
826
802 protected
827 protected
803
828
804 def query_filter_values(custom_field, query)
829 def query_filter_values(custom_field, query)
805 versions_options(custom_field, query.project, true)
830 versions_options(custom_field, query.project, true)
806 end
831 end
807
832
808 def versions_options(custom_field, object, all_statuses=false)
833 def versions_options(custom_field, object, all_statuses=false)
809 if object.is_a?(Array)
834 if object.is_a?(Array)
810 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
835 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
811 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
836 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
812 elsif object.respond_to?(:project) && object.project
837 elsif object.respond_to?(:project) && object.project
813 scope = object.project.shared_versions
838 scope = object.project.shared_versions
814 filtered_versions_options(custom_field, scope, all_statuses)
839 filtered_versions_options(custom_field, scope, all_statuses)
815 elsif object.nil?
840 elsif object.nil?
816 scope = ::Version.visible.where(:sharing => 'system')
841 scope = ::Version.visible.where(:sharing => 'system')
817 filtered_versions_options(custom_field, scope, all_statuses)
842 filtered_versions_options(custom_field, scope, all_statuses)
818 else
843 else
819 []
844 []
820 end
845 end
821 end
846 end
822
847
823 def filtered_versions_options(custom_field, scope, all_statuses=false)
848 def filtered_versions_options(custom_field, scope, all_statuses=false)
824 if !all_statuses && custom_field.version_status.is_a?(Array)
849 if !all_statuses && custom_field.version_status.is_a?(Array)
825 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
850 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
826 if statuses.any?
851 if statuses.any?
827 scope = scope.where(:status => statuses.map(&:to_s))
852 scope = scope.where(:status => statuses.map(&:to_s))
828 end
853 end
829 end
854 end
830 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
855 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
831 end
856 end
832 end
857 end
858
859 class AttachementFormat < Base
860 add 'attachment'
861 self.form_partial = 'custom_fields/formats/attachment'
862 self.is_filter_supported = false
863 self.change_no_details = true
864
865 def set_custom_field_value(custom_field, custom_field_value, value)
866 attachment_present = false
867
868 if value.is_a?(Hash)
869 attachment_present = true
870 value = value.except(:blank)
871
872 if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
873 value = value.values.first
874 end
875
876 if value.key?(:id)
877 value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
878 elsif value[:token].present?
879 if attachment = Attachment.find_by_token(value[:token])
880 value = attachment.id.to_s
881 else
882 value = ''
883 end
884 elsif value.key?(:file)
885 attachment = Attachment.new(:file => value[:file], :author => User.current)
886 if attachment.save
887 value = attachment.id.to_s
888 else
889 value = ''
890 end
891 else
892 attachment_present = false
893 value = ''
894 end
895 elsif value.is_a?(String)
896 value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
897 end
898 custom_field_value.instance_variable_set "@attachment_present", attachment_present
899
900 value
901 end
902
903 def set_custom_field_value_by_id(custom_field, custom_field_value, id)
904 attachment = Attachment.find_by_id(id)
905 if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
906 id.to_s
907 else
908 ''
909 end
910 end
911 private :set_custom_field_value_by_id
912
913 def cast_single_value(custom_field, value, customized=nil)
914 Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
915 end
916
917 def validate_custom_value(custom_value)
918 errors = []
919
920 if custom_value.instance_variable_get("@attachment_present") && custom_value.value.blank?
921 errors << ::I18n.t('activerecord.errors.messages.invalid')
922 end
923
924 errors.uniq
925 end
926
927 def after_save_custom_value(custom_field, custom_value)
928 if custom_value.value_changed?
929 if custom_value.value.present?
930 attachment = Attachment.where(:id => custom_value.value.to_s).first
931 if attachment
932 attachment.container = custom_value
933 attachment.save!
934 end
935 end
936 if custom_value.value_was.present?
937 attachment = Attachment.where(:id => custom_value.value_was.to_s).first
938 if attachment
939 attachment.destroy
940 end
941 end
942 end
943 end
944
945 def edit_tag(view, tag_id, tag_name, custom_value, options={})
946 attachment = nil
947 if custom_value.value.present? #&& custom_value.value == custom_value.value_was
948 attachment = Attachment.find_by_id(custom_value.value)
949 end
950
951 view.hidden_field_tag("#{tag_name}[blank]", "") +
952 view.render(:partial => 'attachments/form',
953 :locals => {
954 :attachment_param => tag_name,
955 :multiple => false,
956 :description => false,
957 :saved_attachments => [attachment].compact,
958 :filedrop => false
959 })
960 end
961 end
833 end
962 end
834 end
963 end
@@ -1,196 +1,211
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 addFile(inputEl, file, eagerUpload) {
4 function addFile(inputEl, file, eagerUpload) {
5 var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
6 var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
7 var maxFiles = ($(inputEl).prop('multiple') == true ? 10 : 1);
5
8
6 if ($('#attachments_fields').children().length < 10) {
9 if (attachmentsFields.children().length < maxFiles) {
7
8 var attachmentId = addFile.nextAttachmentId++;
10 var attachmentId = addFile.nextAttachmentId++;
9
10 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
11 var fileSpan = $('<span>', { id: 'attachments_' + attachmentId });
12 var param = $(inputEl).data('param');
13 if (!param) {param = 'attachments'};
11
14
12 fileSpan.append(
15 fileSpan.append(
13 $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
16 $('<input>', { type: 'text', 'class': 'filename readonly', name: param +'[' + attachmentId + '][filename]', readonly: 'readonly'} ).val(file.name),
14 $('<input>', { type: 'text', 'class': 'description', name: 'attachments[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
17 $('<input>', { type: 'text', 'class': 'description', name: param + '[' + attachmentId + '][description]', maxlength: 255, placeholder: $(inputEl).data('description-placeholder') } ).toggle(!eagerUpload),
18 $('<input>', { type: 'hidden', 'class': 'token', name: param + '[' + attachmentId + '][token]'} ),
15 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
19 $('<a>&nbsp</a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload)
16 ).appendTo('#attachments_fields');
20 ).appendTo(attachmentsFields);
21
22 if ($(inputEl).data('description') == 0) {
23 fileSpan.find('input.description').remove();
24 }
17
25
18 if(eagerUpload) {
26 if(eagerUpload) {
19 ajaxUpload(file, attachmentId, fileSpan, inputEl);
27 ajaxUpload(file, attachmentId, fileSpan, inputEl);
20 }
28 }
21
29
30 addAttachment.toggle(attachmentsFields.children().length < maxFiles);
22 return attachmentId;
31 return attachmentId;
23 }
32 }
24 return null;
33 return null;
25 }
34 }
26
35
27 addFile.nextAttachmentId = 1;
36 addFile.nextAttachmentId = 1;
28
37
29 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
38 function ajaxUpload(file, attachmentId, fileSpan, inputEl) {
30
39
31 function onLoadstart(e) {
40 function onLoadstart(e) {
32 fileSpan.removeClass('ajax-waiting');
41 fileSpan.removeClass('ajax-waiting');
33 fileSpan.addClass('ajax-loading');
42 fileSpan.addClass('ajax-loading');
34 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
43 $('input:submit', $(this).parents('form')).attr('disabled', 'disabled');
35 }
44 }
36
45
37 function onProgress(e) {
46 function onProgress(e) {
38 if(e.lengthComputable) {
47 if(e.lengthComputable) {
39 this.progressbar( 'value', e.loaded * 100 / e.total );
48 this.progressbar( 'value', e.loaded * 100 / e.total );
40 }
49 }
41 }
50 }
42
51
43 function actualUpload(file, attachmentId, fileSpan, inputEl) {
52 function actualUpload(file, attachmentId, fileSpan, inputEl) {
44
53
45 ajaxUpload.uploading++;
54 ajaxUpload.uploading++;
46
55
47 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
56 uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, {
48 loadstartEventHandler: onLoadstart.bind(progressSpan),
57 loadstartEventHandler: onLoadstart.bind(progressSpan),
49 progressEventHandler: onProgress.bind(progressSpan)
58 progressEventHandler: onProgress.bind(progressSpan)
50 })
59 })
51 .done(function(result) {
60 .done(function(result) {
52 progressSpan.progressbar( 'value', 100 ).remove();
61 progressSpan.progressbar( 'value', 100 ).remove();
53 fileSpan.find('input.description, a').css('display', 'inline-block');
62 fileSpan.find('input.description, a').css('display', 'inline-block');
54 })
63 })
55 .fail(function(result) {
64 .fail(function(result) {
56 progressSpan.text(result.statusText);
65 progressSpan.text(result.statusText);
57 }).always(function() {
66 }).always(function() {
58 ajaxUpload.uploading--;
67 ajaxUpload.uploading--;
59 fileSpan.removeClass('ajax-loading');
68 fileSpan.removeClass('ajax-loading');
60 var form = fileSpan.parents('form');
69 var form = fileSpan.parents('form');
61 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
70 if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) {
62 $('input:submit', form).removeAttr('disabled');
71 $('input:submit', form).removeAttr('disabled');
63 }
72 }
64 form.dequeue('upload');
73 form.dequeue('upload');
65 });
74 });
66 }
75 }
67
76
68 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
77 var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename'));
69 progressSpan.progressbar();
78 progressSpan.progressbar();
70 fileSpan.addClass('ajax-waiting');
79 fileSpan.addClass('ajax-waiting');
71
80
72 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
81 var maxSyncUpload = $(inputEl).data('max-concurrent-uploads');
73
82
74 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
83 if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload)
75 actualUpload(file, attachmentId, fileSpan, inputEl);
84 actualUpload(file, attachmentId, fileSpan, inputEl);
76 else
85 else
77 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
86 $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl));
78 }
87 }
79
88
80 ajaxUpload.uploading = 0;
89 ajaxUpload.uploading = 0;
81
90
82 function removeFile() {
91 function removeFile() {
83 $(this).parent('span').remove();
92 $(this).parent('span').remove();
84 return false;
93 return false;
85 }
94 }
86
95
87 function uploadBlob(blob, uploadUrl, attachmentId, options) {
96 function uploadBlob(blob, uploadUrl, attachmentId, options) {
88
97
89 var actualOptions = $.extend({
98 var actualOptions = $.extend({
90 loadstartEventHandler: $.noop,
99 loadstartEventHandler: $.noop,
91 progressEventHandler: $.noop
100 progressEventHandler: $.noop
92 }, options);
101 }, options);
93
102
94 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
103 uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
95 if (blob instanceof window.File) {
104 if (blob instanceof window.File) {
96 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
105 uploadUrl += '&filename=' + encodeURIComponent(blob.name);
97 uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
106 uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
98 }
107 }
99
108
100 return $.ajax(uploadUrl, {
109 return $.ajax(uploadUrl, {
101 type: 'POST',
110 type: 'POST',
102 contentType: 'application/octet-stream',
111 contentType: 'application/octet-stream',
103 beforeSend: function(jqXhr, settings) {
112 beforeSend: function(jqXhr, settings) {
104 jqXhr.setRequestHeader('Accept', 'application/js');
113 jqXhr.setRequestHeader('Accept', 'application/js');
105 // attach proper File object
114 // attach proper File object
106 settings.data = blob;
115 settings.data = blob;
107 },
116 },
108 xhr: function() {
117 xhr: function() {
109 var xhr = $.ajaxSettings.xhr();
118 var xhr = $.ajaxSettings.xhr();
110 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
119 xhr.upload.onloadstart = actualOptions.loadstartEventHandler;
111 xhr.upload.onprogress = actualOptions.progressEventHandler;
120 xhr.upload.onprogress = actualOptions.progressEventHandler;
112 return xhr;
121 return xhr;
113 },
122 },
114 data: blob,
123 data: blob,
115 cache: false,
124 cache: false,
116 processData: false
125 processData: false
117 });
126 });
118 }
127 }
119
128
120 function addInputFiles(inputEl) {
129 function addInputFiles(inputEl) {
130 var attachmentsFields = $(inputEl).closest('.attachments_form').find('.attachments_fields');
131 var addAttachment = $(inputEl).closest('.attachments_form').find('.add_attachment');
121 var clearedFileInput = $(inputEl).clone().val('');
132 var clearedFileInput = $(inputEl).clone().val('');
133 var sizeExceeded = false;
134 var param = $(inputEl).data('param');
135 if (!param) {param = 'attachments'};
122
136
123 if ($.ajaxSettings.xhr().upload && inputEl.files) {
137 if ($.ajaxSettings.xhr().upload && inputEl.files) {
124 // upload files using ajax
138 // upload files using ajax
125 uploadAndAttachFiles(inputEl.files, inputEl);
139 sizeExceeded = uploadAndAttachFiles(inputEl.files, inputEl);
126 $(inputEl).remove();
140 $(inputEl).remove();
127 } else {
141 } else {
128 // browser not supporting the file API, upload on form submission
142 // browser not supporting the file API, upload on form submission
129 var attachmentId;
143 var attachmentId;
130 var aFilename = inputEl.value.split(/\/|\\/);
144 var aFilename = inputEl.value.split(/\/|\\/);
131 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
145 attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false);
132 if (attachmentId) {
146 if (attachmentId) {
133 $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
147 $(inputEl).attr({ name: param + '[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId);
134 }
148 }
135 }
149 }
136
150
137 clearedFileInput.insertAfter('#attachments_fields');
151 clearedFileInput.prependTo(addAttachment);
138 }
152 }
139
153
140 function uploadAndAttachFiles(files, inputEl) {
154 function uploadAndAttachFiles(files, inputEl) {
141
155
142 var maxFileSize = $(inputEl).data('max-file-size');
156 var maxFileSize = $(inputEl).data('max-file-size');
143 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
157 var maxFileSizeExceeded = $(inputEl).data('max-file-size-message');
144
158
145 var sizeExceeded = false;
159 var sizeExceeded = false;
146 $.each(files, function() {
160 $.each(files, function() {
147 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
161 if (this.size && maxFileSize != null && this.size > parseInt(maxFileSize)) {sizeExceeded=true;}
148 });
162 });
149 if (sizeExceeded) {
163 if (sizeExceeded) {
150 window.alert(maxFileSizeExceeded);
164 window.alert(maxFileSizeExceeded);
151 } else {
165 } else {
152 $.each(files, function() {addFile(inputEl, this, true);});
166 $.each(files, function() {addFile(inputEl, this, true);});
153 }
167 }
168 return sizeExceeded;
154 }
169 }
155
170
156 function handleFileDropEvent(e) {
171 function handleFileDropEvent(e) {
157
172
158 $(this).removeClass('fileover');
173 $(this).removeClass('fileover');
159 blockEventPropagation(e);
174 blockEventPropagation(e);
160
175
161 if ($.inArray('Files', e.dataTransfer.types) > -1) {
176 if ($.inArray('Files', e.dataTransfer.types) > -1) {
162 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.file_selector'));
177 uploadAndAttachFiles(e.dataTransfer.files, $('input:file.filedrop').first());
163 }
178 }
164 }
179 }
165
180
166 function dragOverHandler(e) {
181 function dragOverHandler(e) {
167 $(this).addClass('fileover');
182 $(this).addClass('fileover');
168 blockEventPropagation(e);
183 blockEventPropagation(e);
169 }
184 }
170
185
171 function dragOutHandler(e) {
186 function dragOutHandler(e) {
172 $(this).removeClass('fileover');
187 $(this).removeClass('fileover');
173 blockEventPropagation(e);
188 blockEventPropagation(e);
174 }
189 }
175
190
176 function setupFileDrop() {
191 function setupFileDrop() {
177 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
192 if (window.File && window.FileList && window.ProgressEvent && window.FormData) {
178
193
179 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
194 $.event.fixHooks.drop = { props: [ 'dataTransfer' ] };
180
195
181 $('form div.box').has('input:file').each(function() {
196 $('form div.box:not(.filedroplistner)').has('input:file.filedrop').each(function() {
182 $(this).on({
197 $(this).on({
183 dragover: dragOverHandler,
198 dragover: dragOverHandler,
184 dragleave: dragOutHandler,
199 dragleave: dragOutHandler,
185 drop: handleFileDropEvent
200 drop: handleFileDropEvent
186 });
201 }).addClass('filedroplistner');
187 });
202 });
188 }
203 }
189 }
204 }
190
205
191 $(document).ready(setupFileDrop);
206 $(document).ready(setupFileDrop);
192 $(document).ready(function(){
207 $(document).ready(function(){
193 $("input.deleted_attachment").change(function(){
208 $("input.deleted_attachment").change(function(){
194 $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
209 $(this).parents('.existing-attachment').toggleClass('deleted', $(this).is(":checked"));
195 }).change();
210 }).change();
196 });
211 });
@@ -1,1403 +1,1407
1 html {overflow-y:scroll;}
1 html {overflow-y:scroll;}
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
2 body { font-family: Verdana, sans-serif; font-size: 12px; color:#333; margin: 0; padding: 0; min-width: 900px; }
3
3
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
4 h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;}
5 #content h1, h2, h3, h4 {color: #555;}
5 #content h1, h2, h3, h4 {color: #555;}
6 h2, .wiki h1 {font-size: 20px;}
6 h2, .wiki h1 {font-size: 20px;}
7 h3, .wiki h2 {font-size: 16px;}
7 h3, .wiki h2 {font-size: 16px;}
8 h4, .wiki h3 {font-size: 13px;}
8 h4, .wiki h3 {font-size: 13px;}
9 h4 {border-bottom: 1px dotted #bbb;}
9 h4 {border-bottom: 1px dotted #bbb;}
10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
10 pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
11
11
12 /***** Layout *****/
12 /***** Layout *****/
13 #wrapper {background: white;overflow: hidden;}
13 #wrapper {background: white;overflow: hidden;}
14
14
15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
15 #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;}
16 #top-menu ul {margin: 0; padding: 0;}
16 #top-menu ul {margin: 0; padding: 0;}
17 #top-menu li {
17 #top-menu li {
18 float:left;
18 float:left;
19 list-style-type:none;
19 list-style-type:none;
20 margin: 0px 0px 0px 0px;
20 margin: 0px 0px 0px 0px;
21 padding: 0px 0px 0px 0px;
21 padding: 0px 0px 0px 0px;
22 white-space:nowrap;
22 white-space:nowrap;
23 }
23 }
24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
24 #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;}
25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
25 #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; }
26
26
27 #account {float:right;}
27 #account {float:right;}
28
28
29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
29 #header {min-height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 20px 6px; position:relative;}
30 #header a {color:#f8f8f8;}
30 #header a {color:#f8f8f8;}
31 #header h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
31 #header h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
32 #header h1 .breadcrumbs { display:block; font-size: .5em; font-weight: normal; }
32 #header h1 .breadcrumbs { display:block; font-size: .5em; font-weight: normal; }
33 #quick-search {float:right;}
33 #quick-search {float:right;}
34
34
35 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
35 #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px; width: 100%;}
36 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
36 #main-menu ul {margin: 0; padding: 0; width: 100%; white-space: nowrap;}
37 #main-menu li {
37 #main-menu li {
38 float:none;
38 float:none;
39 list-style-type:none;
39 list-style-type:none;
40 margin: 0px 2px 0px 0px;
40 margin: 0px 2px 0px 0px;
41 padding: 0px 0px 0px 0px;
41 padding: 0px 0px 0px 0px;
42 white-space:nowrap;
42 white-space:nowrap;
43 display:inline-block;
43 display:inline-block;
44 }
44 }
45 #main-menu li a {
45 #main-menu li a {
46 display: block;
46 display: block;
47 color: #fff;
47 color: #fff;
48 text-decoration: none;
48 text-decoration: none;
49 font-weight: bold;
49 font-weight: bold;
50 margin: 0;
50 margin: 0;
51 padding: 4px 10px 4px 10px;
51 padding: 4px 10px 4px 10px;
52 }
52 }
53 #main-menu li a:hover {background:#759FCF; color:#fff;}
53 #main-menu li a:hover {background:#759FCF; color:#fff;}
54 #main-menu li:hover ul.menu-children, #main-menu li ul.menu-children.visible {display: block;}
54 #main-menu li:hover ul.menu-children, #main-menu li ul.menu-children.visible {display: block;}
55 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
55 #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;}
56 #main-menu li a.new-object { background-color:#759FCF; }
56 #main-menu li a.new-object { background-color:#759FCF; }
57
57
58 #main-menu .menu-children {
58 #main-menu .menu-children {
59 display: none;
59 display: none;
60 position:absolute;
60 position:absolute;
61 width: inherit;
61 width: inherit;
62 z-index:1;
62 z-index:1;
63 background-color:#fff;
63 background-color:#fff;
64 border-right: 1px solid #759FCF;
64 border-right: 1px solid #759FCF;
65 border-bottom: 1px solid #759FCF;
65 border-bottom: 1px solid #759FCF;
66 border-left: 1px solid #759FCF;
66 border-left: 1px solid #759FCF;
67 }
67 }
68 #main-menu .menu-children li {float:left; clear:both; width:100%;}
68 #main-menu .menu-children li {float:left; clear:both; width:100%;}
69 #main-menu .menu-children li a {color: #555; background-color:#fff; font-weight:normal;}
69 #main-menu .menu-children li a {color: #555; background-color:#fff; font-weight:normal;}
70 #main-menu .menu-children li a:hover {color: #fff; background-color: #759FCF;}
70 #main-menu .menu-children li a:hover {color: #fff; background-color: #759FCF;}
71
71
72 #main-menu .tabs-buttons {
72 #main-menu .tabs-buttons {
73 right: 6px;
73 right: 6px;
74 background-color: transparent;
74 background-color: transparent;
75 border-bottom-color: transparent;
75 border-bottom-color: transparent;
76 }
76 }
77
77
78 #admin-menu ul {margin: 0; padding: 0;}
78 #admin-menu ul {margin: 0; padding: 0;}
79 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
79 #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;}
80
80
81 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
81 #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;}
82 #admin-menu a.projects { background-image: url(../images/projects.png); }
82 #admin-menu a.projects { background-image: url(../images/projects.png); }
83 #admin-menu a.users { background-image: url(../images/user.png); }
83 #admin-menu a.users { background-image: url(../images/user.png); }
84 #admin-menu a.groups { background-image: url(../images/group.png); }
84 #admin-menu a.groups { background-image: url(../images/group.png); }
85 #admin-menu a.roles { background-image: url(../images/database_key.png); }
85 #admin-menu a.roles { background-image: url(../images/database_key.png); }
86 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
86 #admin-menu a.trackers { background-image: url(../images/ticket.png); }
87 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
87 #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); }
88 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
88 #admin-menu a.workflows { background-image: url(../images/ticket_go.png); }
89 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
89 #admin-menu a.custom_fields { background-image: url(../images/textfield.png); }
90 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
90 #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); }
91 #admin-menu a.settings { background-image: url(../images/changeset.png); }
91 #admin-menu a.settings { background-image: url(../images/changeset.png); }
92 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
92 #admin-menu a.plugins { background-image: url(../images/plugin.png); }
93 #admin-menu a.info { background-image: url(../images/help.png); }
93 #admin-menu a.info { background-image: url(../images/help.png); }
94 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
94 #admin-menu a.server_authentication { background-image: url(../images/server_key.png); }
95
95
96 #main {background-color:#EEEEEE;}
96 #main {background-color:#EEEEEE;}
97
97
98 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
98 #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;}
99 * html #sidebar{ width: 22%; }
99 * html #sidebar{ width: 22%; }
100 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
100 #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; }
101 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
101 #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; }
102 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
102 * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; }
103 #sidebar .contextual { margin-right: 1em; }
103 #sidebar .contextual { margin-right: 1em; }
104 #sidebar ul, ul.flat {margin: 0; padding: 0;}
104 #sidebar ul, ul.flat {margin: 0; padding: 0;}
105 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
105 #sidebar ul li, ul.flat li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
106 #sidebar div.wiki ul {margin:inherit; padding-left:40px;}
106 #sidebar div.wiki ul {margin:inherit; padding-left:40px;}
107 #sidebar div.wiki ul li {list-style-type:inherit;}
107 #sidebar div.wiki ul li {list-style-type:inherit;}
108
108
109 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
109 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
110 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
110 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
111 html>body #content { min-height: 600px; }
111 html>body #content { min-height: 600px; }
112 * html body #content { height: 600px; } /* IE */
112 * html body #content { height: 600px; } /* IE */
113
113
114 #main.nosidebar #sidebar{ display: none; }
114 #main.nosidebar #sidebar{ display: none; }
115 #main.nosidebar #content{ width: auto; border-right: 0; }
115 #main.nosidebar #content{ width: auto; border-right: 0; }
116
116
117 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
117 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
118
118
119 #login-form {margin:5em auto 2em auto; padding:20px; width:340px; border:1px solid #FDBF3B; background-color:#FFEBC1; border-radius:4px; box-sizing: border-box;}
119 #login-form {margin:5em auto 2em auto; padding:20px; width:340px; border:1px solid #FDBF3B; background-color:#FFEBC1; border-radius:4px; box-sizing: border-box;}
120 #login-form label {display:block; margin-bottom:5px;}
120 #login-form label {display:block; margin-bottom:5px;}
121 #login-form input[type=text], #login-form input[type=password] {border:1px solid #ccc; border-radius:3px; margin-bottom:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
121 #login-form input[type=text], #login-form input[type=password] {border:1px solid #ccc; border-radius:3px; margin-bottom:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
122 #login-form label {font-weight:bold;}
122 #login-form label {font-weight:bold;}
123 #login-form label[for=autologin] {font-weight:normal;}
123 #login-form label[for=autologin] {font-weight:normal;}
124 #login-form a.lost_password {float:right; font-weight:normal;}
124 #login-form a.lost_password {float:right; font-weight:normal;}
125 #login-form input#openid_url {background:#fff url(../images/openid-bg.gif) no-repeat 4px 50%; padding-left:24px !important;}
125 #login-form input#openid_url {background:#fff url(../images/openid-bg.gif) no-repeat 4px 50%; padding-left:24px !important;}
126 #login-form input#login-submit {margin-top:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
126 #login-form input#login-submit {margin-top:15px; padding:7px; display:block; width:100%; box-sizing: border-box;}
127
127
128 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
128 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
129 div.modal h3.title {display:none;}
129 div.modal h3.title {display:none;}
130 div.modal p.buttons {text-align:right; margin-bottom:0;}
130 div.modal p.buttons {text-align:right; margin-bottom:0;}
131 div.modal .box p {margin: 0.3em 0;}
131 div.modal .box p {margin: 0.3em 0;}
132
132
133 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
133 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
134
134
135 .mobile-show {display: none;}
135 .mobile-show {display: none;}
136
136
137 /***** Links *****/
137 /***** Links *****/
138 a, a:link, a:visited{ color: #169; text-decoration: none; }
138 a, a:link, a:visited{ color: #169; text-decoration: none; }
139 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
139 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
140 a img{ border: 0; }
140 a img{ border: 0; }
141
141
142 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
142 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
143 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
143 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
144 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
144 a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;}
145
145
146 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
146 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
147 #sidebar a.selected:hover {text-decoration:none;}
147 #sidebar a.selected:hover {text-decoration:none;}
148 #admin-menu a {line-height:1.7em;}
148 #admin-menu a {line-height:1.7em;}
149 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
149 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
150
150
151 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
151 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
152 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
152 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
153
153
154 a#toggle-completed-versions {color:#999;}
154 a#toggle-completed-versions {color:#999;}
155
155
156 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
156 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
157
157
158 /***** Tables *****/
158 /***** Tables *****/
159 table.list, .table-list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
159 table.list, .table-list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
160 table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
160 table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
161 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
161 table.list td {text-align:center; vertical-align:top; padding-right:10px;}
162 table.list td.id { width: 2%; text-align: center;}
162 table.list td.id { width: 2%; text-align: center;}
163 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
163 table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles {text-align: left;}
164 table.list td.tick {width:15%}
164 table.list td.tick {width:15%}
165 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
165 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
166 table.list td.checkbox input {padding:0px;}
166 table.list td.checkbox input {padding:0px;}
167 table.list td.buttons, div.buttons { white-space:nowrap; text-align: right; }
167 table.list td.buttons, div.buttons { white-space:nowrap; text-align: right; }
168 table.list td.buttons a, div.buttons a { margin-right: 0.6em; }
168 table.list td.buttons a, div.buttons a { margin-right: 0.6em; }
169 table.list td.buttons img, div.buttons img {vertical-align:middle;}
169 table.list td.buttons img, div.buttons img {vertical-align:middle;}
170 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
170 table.list td.reorder {width:15%; white-space:nowrap; text-align:center; }
171 table.list table.progress td {padding-right:0px;}
171 table.list table.progress td {padding-right:0px;}
172 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
172 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
173 #role-permissions-trackers table.list th {white-space:normal;}
173 #role-permissions-trackers table.list th {white-space:normal;}
174
174
175 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
175 .table-list-cell {display: table-cell; vertical-align: top; padding:2px; }
176
176
177 tr.project td.name a { white-space:nowrap; }
177 tr.project td.name a { white-space:nowrap; }
178 tr.project.closed, tr.project.archived { color: #aaa; }
178 tr.project.closed, tr.project.archived { color: #aaa; }
179 tr.project.closed a, tr.project.archived a { color: #aaa; }
179 tr.project.closed a, tr.project.archived a { color: #aaa; }
180
180
181 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
181 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
182 tr.project.idnt-1 td.name {padding-left: 0.5em;}
182 tr.project.idnt-1 td.name {padding-left: 0.5em;}
183 tr.project.idnt-2 td.name {padding-left: 2em;}
183 tr.project.idnt-2 td.name {padding-left: 2em;}
184 tr.project.idnt-3 td.name {padding-left: 3.5em;}
184 tr.project.idnt-3 td.name {padding-left: 3.5em;}
185 tr.project.idnt-4 td.name {padding-left: 5em;}
185 tr.project.idnt-4 td.name {padding-left: 5em;}
186 tr.project.idnt-5 td.name {padding-left: 6.5em;}
186 tr.project.idnt-5 td.name {padding-left: 6.5em;}
187 tr.project.idnt-6 td.name {padding-left: 8em;}
187 tr.project.idnt-6 td.name {padding-left: 8em;}
188 tr.project.idnt-7 td.name {padding-left: 9.5em;}
188 tr.project.idnt-7 td.name {padding-left: 9.5em;}
189 tr.project.idnt-8 td.name {padding-left: 11em;}
189 tr.project.idnt-8 td.name {padding-left: 11em;}
190 tr.project.idnt-9 td.name {padding-left: 12.5em;}
190 tr.project.idnt-9 td.name {padding-left: 12.5em;}
191
191
192 tr.issue { text-align: center; white-space: nowrap; }
192 tr.issue { text-align: center; white-space: nowrap; }
193 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
193 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.list, tr.issue td.relations, tr.issue td.parent { white-space: normal; }
194 tr.issue td.relations { text-align: left; }
194 tr.issue td.relations { text-align: left; }
195 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
195 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
196 tr.issue td.relations span {white-space: nowrap;}
196 tr.issue td.relations span {white-space: nowrap;}
197 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
197 table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;}
198 table.issues td.description pre {white-space:normal;}
198 table.issues td.description pre {white-space:normal;}
199
199
200 tr.issue.idnt td.subject {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%;}
200 tr.issue.idnt td.subject {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%;}
201 tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
201 tr.issue.idnt-1 td.subject {padding-left: 24px; background-position: 8px 50%;}
202 tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
202 tr.issue.idnt-2 td.subject {padding-left: 40px; background-position: 24px 50%;}
203 tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
203 tr.issue.idnt-3 td.subject {padding-left: 56px; background-position: 40px 50%;}
204 tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
204 tr.issue.idnt-4 td.subject {padding-left: 72px; background-position: 56px 50%;}
205 tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
205 tr.issue.idnt-5 td.subject {padding-left: 88px; background-position: 72px 50%;}
206 tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
206 tr.issue.idnt-6 td.subject {padding-left: 104px; background-position: 88px 50%;}
207 tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
207 tr.issue.idnt-7 td.subject {padding-left: 120px; background-position: 104px 50%;}
208 tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
208 tr.issue.idnt-8 td.subject {padding-left: 136px; background-position: 120px 50%;}
209 tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
209 tr.issue.idnt-9 td.subject {padding-left: 152px; background-position: 136px 50%;}
210
210
211 table.issue-report {table-layout:fixed;}
211 table.issue-report {table-layout:fixed;}
212
212
213 tr.entry { border: 1px solid #f8f8f8; }
213 tr.entry { border: 1px solid #f8f8f8; }
214 tr.entry td { white-space: nowrap; }
214 tr.entry td { white-space: nowrap; }
215 tr.entry td.filename {width:30%; text-align:left;}
215 tr.entry td.filename {width:30%; text-align:left;}
216 tr.entry td.filename_no_report {width:70%; text-align:left;}
216 tr.entry td.filename_no_report {width:70%; text-align:left;}
217 tr.entry td.size { text-align: right; font-size: 90%; }
217 tr.entry td.size { text-align: right; font-size: 90%; }
218 tr.entry td.revision, tr.entry td.author { text-align: center; }
218 tr.entry td.revision, tr.entry td.author { text-align: center; }
219 tr.entry td.age { text-align: right; }
219 tr.entry td.age { text-align: right; }
220 tr.entry.file td.filename a { margin-left: 16px; }
220 tr.entry.file td.filename a { margin-left: 16px; }
221 tr.entry.file td.filename_no_report a { margin-left: 16px; }
221 tr.entry.file td.filename_no_report a { margin-left: 16px; }
222
222
223 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
223 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
224 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
224 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
225
225
226 tr.changeset { height: 20px }
226 tr.changeset { height: 20px }
227 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
227 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
228 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
228 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
229 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
229 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
230 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
230 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
231
231
232 table.files tbody th {text-align:left;}
232 table.files tbody th {text-align:left;}
233 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
233 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
234 table.files tr.file td.digest { font-size: 80%; }
234 table.files tr.file td.digest { font-size: 80%; }
235
235
236 table.members td.roles, table.memberships td.roles { width: 45%; }
236 table.members td.roles, table.memberships td.roles { width: 45%; }
237
237
238 tr.message { height: 2.6em; }
238 tr.message { height: 2.6em; }
239 tr.message td.subject { padding-left: 20px; }
239 tr.message td.subject { padding-left: 20px; }
240 tr.message td.created_on { white-space: nowrap; }
240 tr.message td.created_on { white-space: nowrap; }
241 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
241 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
242 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
242 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
243 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
243 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
244
244
245 tr.version.closed, tr.version.closed a { color: #999; }
245 tr.version.closed, tr.version.closed a { color: #999; }
246 tr.version td.name { padding-left: 20px; }
246 tr.version td.name { padding-left: 20px; }
247 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
247 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
248 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
248 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
249
249
250 tr.user td {width:13%;white-space: nowrap;}
250 tr.user td {width:13%;white-space: nowrap;}
251 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
251 td.username, td.firstname, td.lastname, td.email {text-align:left !important;}
252 tr.user td.email { width:18%; }
252 tr.user td.email { width:18%; }
253 tr.user.locked, tr.user.registered { color: #aaa; }
253 tr.user.locked, tr.user.registered { color: #aaa; }
254 tr.user.locked a, tr.user.registered a { color: #aaa; }
254 tr.user.locked a, tr.user.registered a { color: #aaa; }
255
255
256 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
256 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
257
257
258 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
258 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
259
259
260 tr.time-entry { text-align: center; white-space: nowrap; }
260 tr.time-entry { text-align: center; white-space: nowrap; }
261 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
261 tr.time-entry td.issue, tr.time-entry td.comments, tr.time-entry td.subject, tr.time-entry td.activity { text-align: left; white-space: normal; }
262 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
262 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
263 td.hours .hours-dec { font-size: 0.9em; }
263 td.hours .hours-dec { font-size: 0.9em; }
264
264
265 table.plugins td { vertical-align: middle; }
265 table.plugins td { vertical-align: middle; }
266 table.plugins td.configure { text-align: right; padding-right: 1em; }
266 table.plugins td.configure { text-align: right; padding-right: 1em; }
267 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
267 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
268 table.plugins span.description { display: block; font-size: 0.9em; }
268 table.plugins span.description { display: block; font-size: 0.9em; }
269 table.plugins span.url { display: block; font-size: 0.9em; }
269 table.plugins span.url { display: block; font-size: 0.9em; }
270
270
271 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
271 tr.group td { padding: 0.8em 0 0.5em 0.3em; border-bottom: 1px solid #ccc; text-align:left; }
272 tr.group span.name {font-weight:bold;}
272 tr.group span.name {font-weight:bold;}
273 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
273 tr.group span.count {font-weight:bold; position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;}
274 tr.group span.totals {color: #aaa; font-size: 80%;}
274 tr.group span.totals {color: #aaa; font-size: 80%;}
275 tr.group span.totals .value {font-weight:bold; color:#777;}
275 tr.group span.totals .value {font-weight:bold; color:#777;}
276 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
276 tr.group a.toggle-all { color: #aaa; font-size: 80%; display:none; float:right; margin-right:4px;}
277 tr.group:hover a.toggle-all { display:inline;}
277 tr.group:hover a.toggle-all { display:inline;}
278 a.toggle-all:hover {text-decoration:none;}
278 a.toggle-all:hover {text-decoration:none;}
279
279
280 table.list tbody tr:hover { background-color:#ffffdd; }
280 table.list tbody tr:hover { background-color:#ffffdd; }
281 table.list tbody tr.group:hover { background-color:inherit; }
281 table.list tbody tr.group:hover { background-color:inherit; }
282 table td {padding:2px;}
282 table td {padding:2px;}
283 table p {margin:0;}
283 table p {margin:0;}
284 .odd {background-color:#f6f7f8;}
284 .odd {background-color:#f6f7f8;}
285 .even {background-color: #fff;}
285 .even {background-color: #fff;}
286
286
287 tr.builtin td.name {font-style:italic;}
287 tr.builtin td.name {font-style:italic;}
288
288
289 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
289 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
290 a.sort.asc { background-image: url(../images/sort_asc.png); }
290 a.sort.asc { background-image: url(../images/sort_asc.png); }
291 a.sort.desc { background-image: url(../images/sort_desc.png); }
291 a.sort.desc { background-image: url(../images/sort_desc.png); }
292
292
293 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
293 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
294 table.boards td.last-message {text-align:left;font-size:80%;}
294 table.boards td.last-message {text-align:left;font-size:80%;}
295
295
296 div.table-list.boards .table-list-cell.name {width: 30%;}
296 div.table-list.boards .table-list-cell.name {width: 30%;}
297
297
298 table.messages td.last_message {text-align:left;}
298 table.messages td.last_message {text-align:left;}
299
299
300 #query_form_content {font-size:90%;}
300 #query_form_content {font-size:90%;}
301
301
302 .query_sort_criteria_count {
302 .query_sort_criteria_count {
303 display: inline-block;
303 display: inline-block;
304 min-width: 1em;
304 min-width: 1em;
305 }
305 }
306
306
307 table.query-columns {
307 table.query-columns {
308 border-collapse: collapse;
308 border-collapse: collapse;
309 border: 0;
309 border: 0;
310 }
310 }
311
311
312 table.query-columns td.buttons {
312 table.query-columns td.buttons {
313 vertical-align: middle;
313 vertical-align: middle;
314 text-align: center;
314 text-align: center;
315 }
315 }
316 table.query-columns td.buttons input[type=button] {width:35px;}
316 table.query-columns td.buttons input[type=button] {width:35px;}
317 .query-totals {text-align:right;}
317 .query-totals {text-align:right;}
318 .query-totals>span {margin-left:0.6em;}
318 .query-totals>span {margin-left:0.6em;}
319 .query-totals .value {font-weight:bold;}
319 .query-totals .value {font-weight:bold;}
320 body.controller-issues .query-totals {margin-top:-2.3em;}
320 body.controller-issues .query-totals {margin-top:-2.3em;}
321
321
322 td.center {text-align:center;}
322 td.center {text-align:center;}
323
323
324 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
324 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
325
325
326 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
326 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
327 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
327 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
328 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
328 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
329 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
329 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
330 div.spent_time h3 { background: url(../images/time.png) no-repeat 0% 50%; padding-left: 20px; }
330 div.spent_time h3 { background: url(../images/time.png) no-repeat 0% 50%; padding-left: 20px; }
331
331
332 #watchers select {width: 95%; display: block;}
332 #watchers select {width: 95%; display: block;}
333 #watchers a.delete {opacity: 0.4; margin-left: 5px;}
333 #watchers a.delete {opacity: 0.4; margin-left: 5px;}
334 #watchers a.delete:hover {opacity: 1;}
334 #watchers a.delete:hover {opacity: 1;}
335 #watchers img.gravatar {margin: 0 4px 2px 0;}
335 #watchers img.gravatar {margin: 0 4px 2px 0;}
336
336
337 span#watchers_inputs {overflow:auto; display:block;}
337 span#watchers_inputs {overflow:auto; display:block;}
338 span.search_for_watchers {display:block;}
338 span.search_for_watchers {display:block;}
339 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
339 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
340 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
340 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
341
341
342
342
343 .highlight { background-color: #FCFD8D;}
343 .highlight { background-color: #FCFD8D;}
344 .highlight.token-1 { background-color: #faa;}
344 .highlight.token-1 { background-color: #faa;}
345 .highlight.token-2 { background-color: #afa;}
345 .highlight.token-2 { background-color: #afa;}
346 .highlight.token-3 { background-color: #aaf;}
346 .highlight.token-3 { background-color: #aaf;}
347
347
348 .box{
348 .box{
349 padding:6px;
349 padding:6px;
350 margin-bottom: 10px;
350 margin-bottom: 10px;
351 background-color:#f6f6f6;
351 background-color:#f6f6f6;
352 color:#505050;
352 color:#505050;
353 line-height:1.5em;
353 line-height:1.5em;
354 border: 1px solid #e4e4e4;
354 border: 1px solid #e4e4e4;
355 word-wrap: break-word;
355 word-wrap: break-word;
356 border-radius: 3px;
356 border-radius: 3px;
357 }
357 }
358
358
359 div.square {
359 div.square {
360 border: 1px solid #999;
360 border: 1px solid #999;
361 float: left;
361 float: left;
362 margin: .3em .4em 0 .4em;
362 margin: .3em .4em 0 .4em;
363 overflow: hidden;
363 overflow: hidden;
364 width: .6em; height: .6em;
364 width: .6em; height: .6em;
365 }
365 }
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
366 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin:5px 0px; padding-left: 10px; font-size:0.9em;}
367 .contextual input, .contextual select {font-size:0.9em;}
367 .contextual input, .contextual select {font-size:0.9em;}
368 .message .contextual { margin-top: 0; }
368 .message .contextual { margin-top: 0; }
369
369
370 .splitcontent {overflow:auto;}
370 .splitcontent {overflow:auto;}
371 .splitcontentleft{float:left; width:49%;}
371 .splitcontentleft{float:left; width:49%;}
372 .splitcontentright{float:right; width:49%;}
372 .splitcontentright{float:right; width:49%;}
373 form {display: inline;}
373 form {display: inline;}
374 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
374 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
375 input[type="submit"] { -webkit-appearance: button; }
375 input[type="submit"] { -webkit-appearance: button; }
376 fieldset {border: 1px solid #e4e4e4; margin:0;}
376 fieldset {border: 1px solid #e4e4e4; margin:0;}
377 legend {color: #333;}
377 legend {color: #333;}
378 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
378 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
379 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
379 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
380 blockquote blockquote { margin-left: 0;}
380 blockquote blockquote { margin-left: 0;}
381 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
381 abbr, span.field-description[title] { border-bottom: 1px dotted #aaa; cursor: help; }
382 textarea.wiki-edit {width:99%; resize:vertical;}
382 textarea.wiki-edit {width:99%; resize:vertical;}
383 body.textarea-monospace textarea.wiki-edit {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace; font-size: 12px;}
383 body.textarea-monospace textarea.wiki-edit {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace; font-size: 12px;}
384 body.textarea-proportional textarea.wiki-edit {font-family: Verdana, sans-serif; font-size: 12px;}
384 body.textarea-proportional textarea.wiki-edit {font-family: Verdana, sans-serif; font-size: 12px;}
385 li p {margin-top: 0;}
385 li p {margin-top: 0;}
386 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
386 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px; border: 1px solid #d7d7d7; border-radius:3px;}
387 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
387 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
388 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
388 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
389 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
389 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
390 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
390 .ltr {direction:ltr !important; unicode-bidi:bidi-override;}
391 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
391 .rtl {direction:rtl !important; unicode-bidi:bidi-override;}
392
392
393 div.issue div.subject div div { padding-left: 16px; }
393 div.issue div.subject div div { padding-left: 16px; }
394 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
394 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
395 div.issue div.subject>div>p { margin-top: 0.5em; }
395 div.issue div.subject>div>p { margin-top: 0.5em; }
396 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
396 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
397 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
397 div.issue span.private, div.journal span.private { position:relative; bottom: 2px; text-transform: uppercase; background: #d22; color: #fff; font-weight:bold; padding: 0px 2px 0px 2px; font-size: 60%; margin-right: 2px; border-radius: 2px;}
398 div.issue .next-prev-links {color:#999;}
398 div.issue .next-prev-links {color:#999;}
399 div.issue .attributes {margin-top: 2em;}
399 div.issue .attributes {margin-top: 2em;}
400 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
400 div.issue .attribute {padding-left:180px; clear:left; min-height: 1.8em;}
401 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
401 div.issue .attribute .label {width: 170px; margin-left:-180px; font-weight:bold; float:left;}
402 div.issue.overdue .due-date .value { color: #c22; }
402 div.issue.overdue .due-date .value { color: #c22; }
403
403
404 #issue_tree table.issues, #relations table.issues { border: 0; }
404 #issue_tree table.issues, #relations table.issues { border: 0; }
405 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
405 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
406 #relations td.buttons {padding:0;}
406 #relations td.buttons {padding:0;}
407
407
408 fieldset.collapsible {border-width: 1px 0 0 0;}
408 fieldset.collapsible {border-width: 1px 0 0 0;}
409 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
409 fieldset.collapsible>legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
410 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
410 fieldset.collapsible.collapsed>legend { background-image: url(../images/arrow_collapsed.png); }
411
411
412 fieldset#date-range p { margin: 2px 0 2px 0; }
412 fieldset#date-range p { margin: 2px 0 2px 0; }
413 fieldset#filters table { border-collapse: collapse; }
413 fieldset#filters table { border-collapse: collapse; }
414 fieldset#filters table td { padding: 0; vertical-align: middle; }
414 fieldset#filters table td { padding: 0; vertical-align: middle; }
415 fieldset#filters tr.filter { height: 2.1em; }
415 fieldset#filters tr.filter { height: 2.1em; }
416 fieldset#filters td.field { width:230px; }
416 fieldset#filters td.field { width:230px; }
417 fieldset#filters td.operator { width:130px; }
417 fieldset#filters td.operator { width:130px; }
418 fieldset#filters td.operator select {max-width:120px;}
418 fieldset#filters td.operator select {max-width:120px;}
419 fieldset#filters td.values { white-space:nowrap; }
419 fieldset#filters td.values { white-space:nowrap; }
420 fieldset#filters td.values select {min-width:130px; max-width:200px;}
420 fieldset#filters td.values select {min-width:130px; max-width:200px;}
421 fieldset#filters td.values input {height:1em;}
421 fieldset#filters td.values input {height:1em;}
422
422
423 #filters-table {width:60%; float:left;}
423 #filters-table {width:60%; float:left;}
424 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
424 .add-filter {width:35%; float:right; text-align: right; vertical-align: top;}
425
425
426 #issue_is_private_wrap {float:right; margin-right:1em;}
426 #issue_is_private_wrap {float:right; margin-right:1em;}
427 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:16px; margin-left:0; margin-right:5px; cursor:pointer;}
427 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:16px; margin-left:0; margin-right:5px; cursor:pointer;}
428 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
428 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
429
429
430 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
430 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
431 div#issue-changesets div.changeset { padding: 4px;}
431 div#issue-changesets div.changeset { padding: 4px;}
432 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
432 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
433 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
433 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
434
434
435 div.journal {overflow:auto;}
435 div.journal {overflow:auto;}
436 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
436 div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;}
437 div.journal ul.details, ul.revision-info {color:#959595; margin-bottom: 1.5em;}
437 div.journal ul.details, ul.revision-info {color:#959595; margin-bottom: 1.5em;}
438 div.journal ul.details a, ul.revision-info a {color:#70A7CD;}
438 div.journal ul.details a, ul.revision-info a {color:#70A7CD;}
439 div.journal ul.details a:hover, ul.revision-info a:hover {color:#D14848;}
439 div.journal ul.details a:hover, ul.revision-info a:hover {color:#D14848;}
440
440
441 div#activity dl, #search-results { margin-left: 2em; }
441 div#activity dl, #search-results { margin-left: 2em; }
442 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
442 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
443 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
443 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
444 div#activity dt.me .time { border-bottom: 1px solid #999; }
444 div#activity dt.me .time { border-bottom: 1px solid #999; }
445 div#activity dt .time { color: #777; font-size: 80%; }
445 div#activity dt .time { color: #777; font-size: 80%; }
446 div#activity dd .description, #search-results dd .description { font-style: italic; }
446 div#activity dd .description, #search-results dd .description { font-style: italic; }
447 div#activity span.project:after, #search-results span.project:after { content: " -"; }
447 div#activity span.project:after, #search-results span.project:after { content: " -"; }
448 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
448 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
449 div#activity dt.grouped {margin-left:5em;}
449 div#activity dt.grouped {margin-left:5em;}
450 div#activity dd.grouped {margin-left:9em;}
450 div#activity dd.grouped {margin-left:9em;}
451 div#activity dt { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; height: 18px;}
451 div#activity dt { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; height: 18px;}
452
452
453 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
453 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
454
454
455 div#search-results-counts {float:right;}
455 div#search-results-counts {float:right;}
456 div#search-results-counts ul { margin-top: 0.5em; }
456 div#search-results-counts ul { margin-top: 0.5em; }
457 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
457 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
458
458
459 dt.issue { background-image: url(../images/ticket.png); }
459 dt.issue { background-image: url(../images/ticket.png); }
460 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
460 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
461 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
461 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
462 dt.issue-note { background-image: url(../images/ticket_note.png); }
462 dt.issue-note { background-image: url(../images/ticket_note.png); }
463 dt.changeset { background-image: url(../images/changeset.png); }
463 dt.changeset { background-image: url(../images/changeset.png); }
464 dt.news { background-image: url(../images/news.png); }
464 dt.news { background-image: url(../images/news.png); }
465 dt.message { background-image: url(../images/message.png); }
465 dt.message { background-image: url(../images/message.png); }
466 dt.reply { background-image: url(../images/comments.png); }
466 dt.reply { background-image: url(../images/comments.png); }
467 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
467 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
468 dt.attachment { background-image: url(../images/attachment.png); }
468 dt.attachment { background-image: url(../images/attachment.png); }
469 dt.document { background-image: url(../images/document.png); }
469 dt.document { background-image: url(../images/document.png); }
470 dt.project { background-image: url(../images/projects.png); }
470 dt.project { background-image: url(../images/projects.png); }
471 dt.time-entry { background-image: url(../images/time.png); }
471 dt.time-entry { background-image: url(../images/time.png); }
472
472
473 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
473 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
474
474
475 div#roadmap .related-issues { margin-bottom: 1em; }
475 div#roadmap .related-issues { margin-bottom: 1em; }
476 div#roadmap .related-issues td.checkbox { display: none; }
476 div#roadmap .related-issues td.checkbox { display: none; }
477 div#roadmap .wiki h1:first-child { display: none; }
477 div#roadmap .wiki h1:first-child { display: none; }
478 div#roadmap .wiki h1 { font-size: 120%; }
478 div#roadmap .wiki h1 { font-size: 120%; }
479 div#roadmap .wiki h2 { font-size: 110%; }
479 div#roadmap .wiki h2 { font-size: 110%; }
480 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
480 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
481
481
482 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
482 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
483 div#version-summary fieldset { margin-bottom: 1em; }
483 div#version-summary fieldset { margin-bottom: 1em; }
484 div#version-summary fieldset.time-tracking table { width:100%; }
484 div#version-summary fieldset.time-tracking table { width:100%; }
485 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
485 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
486
486
487 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
487 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
488 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
488 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
489 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
489 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
490 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
490 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
491 table#time-report .hours-dec { font-size: 0.9em; }
491 table#time-report .hours-dec { font-size: 0.9em; }
492
492
493 div.wiki-page .contextual a {opacity: 0.4}
493 div.wiki-page .contextual a {opacity: 0.4}
494 div.wiki-page .contextual a:hover {opacity: 1}
494 div.wiki-page .contextual a:hover {opacity: 1}
495
495
496 form .attributes select { width: 60%; }
496 form .attributes select { width: 60%; }
497 form .attributes select + a.icon-only { vertical-align: middle; margin-left: 4px; }
497 form .attributes select + a.icon-only { vertical-align: middle; margin-left: 4px; }
498 input#issue_subject, input#document_title { width: 99%; }
498 input#issue_subject, input#document_title { width: 99%; }
499 select#issue_done_ratio { width: 95px; }
499 select#issue_done_ratio { width: 95px; }
500
500
501 ul.projects {margin:0; padding-left:1em;}
501 ul.projects {margin:0; padding-left:1em;}
502 ul.projects ul {padding-left:1.6em;}
502 ul.projects ul {padding-left:1.6em;}
503 ul.projects.root {margin:0; padding:0;}
503 ul.projects.root {margin:0; padding:0;}
504 ul.projects li {list-style-type:none;}
504 ul.projects li {list-style-type:none;}
505
505
506 #projects-index {
506 #projects-index {
507 column-count: auto;
507 column-count: auto;
508 column-width: 400px;
508 column-width: 400px;
509 -webkit-column-count: auto;
509 -webkit-column-count: auto;
510 -webkit-column-width: 400px;
510 -webkit-column-width: 400px;
511 -webkit-column-gap : 0.5rem;
511 -webkit-column-gap : 0.5rem;
512 -moz-column-count: auto;
512 -moz-column-count: auto;
513 -moz-column-width: 400px;
513 -moz-column-width: 400px;
514 -moz-column-gap : 0.5rem;
514 -moz-column-gap : 0.5rem;
515 }
515 }
516 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
516 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
517 #projects-index ul.projects li.root {margin-bottom: 1em;}
517 #projects-index ul.projects li.root {margin-bottom: 1em;}
518 #projects-index ul.projects li.child {margin-top: 1em;}
518 #projects-index ul.projects li.child {margin-top: 1em;}
519 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
519 #projects-index ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
520 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
520 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
521
521
522 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
522 #notified-projects>ul, #tracker_project_ids>ul, #custom_field_project_ids>ul {max-height:250px; overflow-y:auto;}
523
523
524 #related-issues li img {vertical-align:middle;}
524 #related-issues li img {vertical-align:middle;}
525
525
526 ul.properties {padding:0; font-size: 0.9em; color: #777;}
526 ul.properties {padding:0; font-size: 0.9em; color: #777;}
527 ul.properties li {list-style-type:none;}
527 ul.properties li {list-style-type:none;}
528 ul.properties li span {font-style:italic;}
528 ul.properties li span {font-style:italic;}
529
529
530 .total-hours { font-size: 110%; font-weight: bold; }
530 .total-hours { font-size: 110%; font-weight: bold; }
531 .total-hours span.hours-int { font-size: 120%; }
531 .total-hours span.hours-int { font-size: 120%; }
532
532
533 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em; position: relative;}
533 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em; position: relative;}
534 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
534 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
535
535
536 #workflow_copy_form select { width: 200px; }
536 #workflow_copy_form select { width: 200px; }
537 table.transitions td.enabled {background: #bfb;}
537 table.transitions td.enabled {background: #bfb;}
538 #workflow_form table select {font-size:90%; max-width:100px;}
538 #workflow_form table select {font-size:90%; max-width:100px;}
539 table.fields_permissions td.readonly {background:#ddd;}
539 table.fields_permissions td.readonly {background:#ddd;}
540 table.fields_permissions td.required {background:#d88;}
540 table.fields_permissions td.required {background:#d88;}
541
541
542 select.expandable {vertical-align:top;}
542 select.expandable {vertical-align:top;}
543
543
544 textarea#custom_field_possible_values {width: 95%; resize:vertical}
544 textarea#custom_field_possible_values {width: 95%; resize:vertical}
545 textarea#custom_field_default_value {width: 95%; resize:vertical}
545 textarea#custom_field_default_value {width: 95%; resize:vertical}
546 .sort-handle {display:inline-block; vertical-align:middle;}
546 .sort-handle {display:inline-block; vertical-align:middle;}
547
547
548 input#content_comments {width: 99%}
548 input#content_comments {width: 99%}
549
549
550 span.pagination {margin-left:3px; color:#888; display:block;}
550 span.pagination {margin-left:3px; color:#888; display:block;}
551 .pagination ul.pages {
551 .pagination ul.pages {
552 margin: 0 5px 0 0;
552 margin: 0 5px 0 0;
553 padding: 0;
553 padding: 0;
554 display: inline;
554 display: inline;
555 }
555 }
556 .pagination ul.pages li {
556 .pagination ul.pages li {
557 display: inline-block;
557 display: inline-block;
558 padding: 0;
558 padding: 0;
559 border: 1px solid #ddd;
559 border: 1px solid #ddd;
560 margin-left: -1px;
560 margin-left: -1px;
561 line-height: 2em;
561 line-height: 2em;
562 margin-bottom: 1em;
562 margin-bottom: 1em;
563 white-space: nowrap;
563 white-space: nowrap;
564 text-align: center;
564 text-align: center;
565 }
565 }
566 .pagination ul.pages li a,
566 .pagination ul.pages li a,
567 .pagination ul.pages li span {
567 .pagination ul.pages li span {
568 padding: 3px 8px;
568 padding: 3px 8px;
569 }
569 }
570 .pagination ul.pages li:first-child {
570 .pagination ul.pages li:first-child {
571 border-top-left-radius: 4px;
571 border-top-left-radius: 4px;
572 border-bottom-left-radius: 4px;
572 border-bottom-left-radius: 4px;
573 }
573 }
574 .pagination ul.pages li:last-child {
574 .pagination ul.pages li:last-child {
575 border-top-right-radius: 4px;
575 border-top-right-radius: 4px;
576 border-bottom-right-radius: 4px;
576 border-bottom-right-radius: 4px;
577 }
577 }
578 .pagination ul.pages li.current {
578 .pagination ul.pages li.current {
579 color: white;
579 color: white;
580 background-color: #628DB6;
580 background-color: #628DB6;
581 border-color: #628DB6;
581 border-color: #628DB6;
582 }
582 }
583 .pagination ul.pages li.page:hover {
583 .pagination ul.pages li.page:hover {
584 background-color: #ddd;
584 background-color: #ddd;
585 }
585 }
586 .pagination ul.pages li.page a:hover,
586 .pagination ul.pages li.page a:hover,
587 .pagination ul.pages li.page a:active {
587 .pagination ul.pages li.page a:active {
588 color: #169;
588 color: #169;
589 text-decoration: inherit;
589 text-decoration: inherit;
590 }
590 }
591 .pagination .per-page span.selected {
591 .pagination .per-page span.selected {
592 font-weight: bold;
592 font-weight: bold;
593 }
593 }
594 span.pagination>span {white-space:nowrap;}
594 span.pagination>span {white-space:nowrap;}
595
595
596 #search-form fieldset p {margin:0.2em 0;}
596 #search-form fieldset p {margin:0.2em 0;}
597
597
598 /***** Tabular forms ******/
598 /***** Tabular forms ******/
599 .tabular p{
599 .tabular p{
600 margin: 0;
600 margin: 0;
601 padding: 3px 0 3px 0;
601 padding: 3px 0 3px 0;
602 padding-left: 180px; /* width of left column containing the label elements */
602 padding-left: 180px; /* width of left column containing the label elements */
603 min-height: 1.8em;
603 line-height: 2em;
604 clear:left;
604 clear:left;
605 }
605 }
606
606
607 html>body .tabular p {overflow:hidden;}
607 html>body .tabular p {overflow:hidden;}
608
608
609 .tabular input, .tabular select {max-width:95%}
609 .tabular input, .tabular select {max-width:95%}
610 .tabular textarea {width:95%; resize:vertical;}
610 .tabular textarea {width:95%; resize:vertical;}
611
611
612 .tabular label{
612 .tabular label{
613 font-weight: bold;
613 font-weight: bold;
614 float: left;
614 float: left;
615 text-align: right;
615 text-align: right;
616 /* width of left column */
616 /* width of left column */
617 margin-left: -180px;
617 margin-left: -180px;
618 /* width of labels. Should be smaller than left column to create some right margin */
618 /* width of labels. Should be smaller than left column to create some right margin */
619 width: 175px;
619 width: 175px;
620 }
620 }
621
621
622 .tabular label.floating{
622 .tabular label.floating{
623 font-weight: normal;
623 font-weight: normal;
624 margin-left: 0px;
624 margin-left: 0px;
625 text-align: left;
625 text-align: left;
626 width: 270px;
626 width: 270px;
627 }
627 }
628
628
629 label.block {
630 display: block;
631 width: auto !important;
632 }
633
629 .tabular label.block{
634 .tabular label.block{
630 font-weight: normal;
635 font-weight: normal;
631 margin-left: 0px !important;
636 margin-left: 0px !important;
632 text-align: left;
637 text-align: left;
633 float: none;
638 float: none;
634 display: block;
635 width: auto !important;
636 }
639 }
637
640
638 .tabular label.inline{
641 .tabular label.inline{
639 font-weight: normal;
642 font-weight: normal;
640 float:none;
643 float:none;
641 margin-left: 5px !important;
644 margin-left: 5px !important;
642 width: auto;
645 width: auto;
643 }
646 }
644
647
645 label.no-css {
648 label.no-css {
646 font-weight: inherit;
649 font-weight: inherit;
647 float:none;
650 float:none;
648 text-align:left;
651 text-align:left;
649 margin-left:0px;
652 margin-left:0px;
650 width:auto;
653 width:auto;
651 }
654 }
652 input#time_entry_comments { width: 90%;}
655 input#time_entry_comments { width: 90%;}
653
656
654 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
657 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
655
658
656 .tabular.settings p{ padding-left: 300px; }
659 .tabular.settings p{ padding-left: 300px; }
657 .tabular.settings label{ margin-left: -300px; width: 295px; }
660 .tabular.settings label{ margin-left: -300px; width: 295px; }
658 .tabular.settings textarea { width: 99%; }
661 .tabular.settings textarea { width: 99%; }
659
662
660 .settings.enabled_scm table {width:100%}
663 .settings.enabled_scm table {width:100%}
661 .settings.enabled_scm td.scm_name{ font-weight: bold; }
664 .settings.enabled_scm td.scm_name{ font-weight: bold; }
662
665
663 fieldset.settings label { display: block; }
666 fieldset.settings label { display: block; }
664 fieldset#notified_events .parent { padding-left: 20px; }
667 fieldset#notified_events .parent { padding-left: 20px; }
665
668
666 span.required {color: #bb0000;}
669 span.required {color: #bb0000;}
667 .summary {font-style: italic;}
670 .summary {font-style: italic;}
668
671
669 .check_box_group {
672 .check_box_group {
670 display:block;
673 display:block;
671 width:95%;
674 width:95%;
672 max-height:300px;
675 max-height:300px;
673 overflow-y:auto;
676 overflow-y:auto;
674 padding:2px 4px 4px 2px;
677 padding:2px 4px 4px 2px;
675 background:#fff;
678 background:#fff;
676 border:1px solid #9EB1C2;
679 border:1px solid #9EB1C2;
677 border-radius:2px
680 border-radius:2px
678 }
681 }
679 .check_box_group label {
682 .check_box_group label {
680 font-weight: normal;
683 font-weight: normal;
681 margin-left: 0px !important;
684 margin-left: 0px !important;
682 text-align: left;
685 text-align: left;
683 float: none;
686 float: none;
684 display: block;
687 display: block;
685 width: auto;
688 width: auto;
686 }
689 }
687 .check_box_group.bool_cf {border:0; background:inherit;}
690 .check_box_group.bool_cf {border:0; background:inherit;}
688 .check_box_group.bool_cf label {display: inline;}
691 .check_box_group.bool_cf label {display: inline;}
689
692
690 #attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
693 .attachments_fields input.description, #existing-attachments input.description {margin-left:4px; width:340px;}
691 #attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
694 .attachments_fields>span, #existing-attachments>span {display:block; white-space:nowrap;}
692 #attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
695 .attachments_fields input.filename, #existing-attachments .filename {border:0; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;}
693 #attachments_fields input.filename {height:1.8em;}
696 .tabular input.filename {max-width:75% !important;}
694 #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
697 .attachments_fields input.filename {height:1.8em;}
695 #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
698 .attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;}
696 #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
699 .attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;}
700 .attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; }
697 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
701 a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;}
698 a.remove-upload:hover {text-decoration:none !important;}
702 a.remove-upload:hover {text-decoration:none !important;}
699 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
703 .existing-attachment.deleted .filename {text-decoration:line-through; color:#999 !important;}
700
704
701 div.fileover { background-color: lavender; }
705 div.fileover { background-color: lavender; }
702
706
703 div.attachments { margin: 12px 0; }
707 div.attachments { margin: 12px 0; }
704 div.attachments p { margin:4px 0 2px 0; }
708 div.attachments p { margin:4px 0 2px 0; }
705 div.attachments img { vertical-align: middle; }
709 div.attachments img { vertical-align: middle; }
706 div.attachments span.author { font-size: 0.9em; color: #888; }
710 div.attachments span.author { font-size: 0.9em; color: #888; }
707
711
708 div.thumbnails {margin:0.6em;}
712 div.thumbnails {margin:0.6em;}
709 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
713 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
710 div.thumbnails img {margin: 3px; vertical-align: middle;}
714 div.thumbnails img {margin: 3px; vertical-align: middle;}
711 #history div.thumbnails {margin-left: 2em;}
715 #history div.thumbnails {margin-left: 2em;}
712
716
713 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
717 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
714 .other-formats span + span:before { content: "| "; }
718 .other-formats span + span:before { content: "| "; }
715
719
716 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
720 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
717
721
718 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
722 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
719 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
723 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
720
724
721 textarea.text_cf {width:95%; resize:vertical;}
725 textarea.text_cf {width:95%; resize:vertical;}
722 input.string_cf, input.link_cf {width:95%;}
726 input.string_cf, input.link_cf {width:95%;}
723 select.bool_cf {width:auto !important;}
727 select.bool_cf {width:auto !important;}
724
728
725 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
729 #tab-content-modules fieldset p {margin:3px 0 4px 0;}
726
730
727 #tab-content-users .splitcontentleft {width: 64%;}
731 #tab-content-users .splitcontentleft {width: 64%;}
728 #tab-content-users .splitcontentright {width: 34%;}
732 #tab-content-users .splitcontentright {width: 34%;}
729 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
733 #tab-content-users fieldset {padding:1em; margin-bottom: 1em;}
730 #tab-content-users fieldset legend {font-weight: bold;}
734 #tab-content-users fieldset legend {font-weight: bold;}
731 #tab-content-users fieldset label {display: block;}
735 #tab-content-users fieldset label {display: block;}
732 #tab-content-users #principals {max-height: 400px; overflow: auto;}
736 #tab-content-users #principals {max-height: 400px; overflow: auto;}
733
737
734 #users_for_watcher {height: 200px; overflow:auto;}
738 #users_for_watcher {height: 200px; overflow:auto;}
735 #users_for_watcher label {display: block;}
739 #users_for_watcher label {display: block;}
736
740
737 table.members td.name {padding-left: 20px;}
741 table.members td.name {padding-left: 20px;}
738 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
742 table.members td.group, table.members td.groupnonmember, table.members td.groupanonymous {background: url(../images/group.png) no-repeat 0% 1px;}
739
743
740 input#principal_search, input#user_search {width:90%}
744 input#principal_search, input#user_search {width:90%}
741 .roles-selection label {display:inline-block; width:210px;}
745 .roles-selection label {display:inline-block; width:210px;}
742
746
743 input.autocomplete {
747 input.autocomplete {
744 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
748 background: #fff url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px !important;
745 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
749 border:1px solid #9EB1C2; border-radius:2px; height:1.5em;
746 }
750 }
747 input.autocomplete.ajax-loading {
751 input.autocomplete.ajax-loading {
748 background-image: url(../images/loading.gif);
752 background-image: url(../images/loading.gif);
749 }
753 }
750
754
751 .role-visibility {padding-left:2em;}
755 .role-visibility {padding-left:2em;}
752
756
753 .objects-selection {
757 .objects-selection {
754 height: 300px;
758 height: 300px;
755 overflow: auto;
759 overflow: auto;
756 margin-bottom: 1em;
760 margin-bottom: 1em;
757 }
761 }
758
762
759 .objects-selection label {
763 .objects-selection label {
760 display: block;
764 display: block;
761 }
765 }
762
766
763 .objects-selection>div, #user_group_ids {
767 .objects-selection>div, #user_group_ids {
764 column-count: auto;
768 column-count: auto;
765 column-width: 200px;
769 column-width: 200px;
766 -webkit-column-count: auto;
770 -webkit-column-count: auto;
767 -webkit-column-width: 200px;
771 -webkit-column-width: 200px;
768 -webkit-column-gap : 0.5rem;
772 -webkit-column-gap : 0.5rem;
769 -webkit-column-rule: 1px solid #ccc;
773 -webkit-column-rule: 1px solid #ccc;
770 -moz-column-count: auto;
774 -moz-column-count: auto;
771 -moz-column-width: 200px;
775 -moz-column-width: 200px;
772 -moz-column-gap : 0.5rem;
776 -moz-column-gap : 0.5rem;
773 -moz-column-rule: 1px solid #ccc;
777 -moz-column-rule: 1px solid #ccc;
774 }
778 }
775
779
776 /***** Flash & error messages ****/
780 /***** Flash & error messages ****/
777 #errorExplanation, div.flash, .nodata, .warning, .conflict {
781 #errorExplanation, div.flash, .nodata, .warning, .conflict {
778 padding: 6px 4px 6px 30px;
782 padding: 6px 4px 6px 30px;
779 margin-bottom: 12px;
783 margin-bottom: 12px;
780 font-size: 1.1em;
784 font-size: 1.1em;
781 border: 1px solid;
785 border: 1px solid;
782 border-radius: 3px;
786 border-radius: 3px;
783 }
787 }
784
788
785 div.flash {margin-top: 8px;}
789 div.flash {margin-top: 8px;}
786
790
787 div.flash.error, #errorExplanation {
791 div.flash.error, #errorExplanation {
788 background: url(../images/exclamation.png) 8px 50% no-repeat;
792 background: url(../images/exclamation.png) 8px 50% no-repeat;
789 background-color: #ffe3e3;
793 background-color: #ffe3e3;
790 border-color: #d88;
794 border-color: #d88;
791 color: #880000;
795 color: #880000;
792 }
796 }
793
797
794 div.flash.notice {
798 div.flash.notice {
795 background: url(../images/true.png) 8px 5px no-repeat;
799 background: url(../images/true.png) 8px 5px no-repeat;
796 background-color: #dfffdf;
800 background-color: #dfffdf;
797 border-color: #9fcf9f;
801 border-color: #9fcf9f;
798 color: #005f00;
802 color: #005f00;
799 }
803 }
800
804
801 div.flash.warning, .conflict {
805 div.flash.warning, .conflict {
802 background: url(../images/warning.png) 8px 5px no-repeat;
806 background: url(../images/warning.png) 8px 5px no-repeat;
803 background-color: #F3EDD1;
807 background-color: #F3EDD1;
804 border-color: #eadbbc;
808 border-color: #eadbbc;
805 color: #A6750C;
809 color: #A6750C;
806 text-align: left;
810 text-align: left;
807 }
811 }
808
812
809 .nodata, .warning {
813 .nodata, .warning {
810 text-align: center;
814 text-align: center;
811 background-color: #F3EDD1;
815 background-color: #F3EDD1;
812 border-color: #eadbbc;
816 border-color: #eadbbc;
813 color: #A6750C;
817 color: #A6750C;
814 }
818 }
815
819
816 #errorExplanation ul { font-size: 0.9em;}
820 #errorExplanation ul { font-size: 0.9em;}
817 #errorExplanation h2, #errorExplanation p { display: none; }
821 #errorExplanation h2, #errorExplanation p { display: none; }
818
822
819 .conflict-details {font-size:80%;}
823 .conflict-details {font-size:80%;}
820
824
821 /***** Ajax indicator ******/
825 /***** Ajax indicator ******/
822 #ajax-indicator {
826 #ajax-indicator {
823 position: absolute; /* fixed not supported by IE */
827 position: absolute; /* fixed not supported by IE */
824 background-color:#eee;
828 background-color:#eee;
825 border: 1px solid #bbb;
829 border: 1px solid #bbb;
826 top:35%;
830 top:35%;
827 left:40%;
831 left:40%;
828 width:20%;
832 width:20%;
829 font-weight:bold;
833 font-weight:bold;
830 text-align:center;
834 text-align:center;
831 padding:0.6em;
835 padding:0.6em;
832 z-index:100;
836 z-index:100;
833 opacity: 0.5;
837 opacity: 0.5;
834 }
838 }
835
839
836 html>body #ajax-indicator { position: fixed; }
840 html>body #ajax-indicator { position: fixed; }
837
841
838 #ajax-indicator span {
842 #ajax-indicator span {
839 background-position: 0% 40%;
843 background-position: 0% 40%;
840 background-repeat: no-repeat;
844 background-repeat: no-repeat;
841 background-image: url(../images/loading.gif);
845 background-image: url(../images/loading.gif);
842 padding-left: 26px;
846 padding-left: 26px;
843 vertical-align: bottom;
847 vertical-align: bottom;
844 }
848 }
845
849
846 /***** Calendar *****/
850 /***** Calendar *****/
847 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
851 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
848 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
852 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
849 table.cal thead th.week-number {width: auto;}
853 table.cal thead th.week-number {width: auto;}
850 table.cal tbody tr {height: 100px;}
854 table.cal tbody tr {height: 100px;}
851 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
855 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
852 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
856 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
853 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
857 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
854 table.cal td.odd p.day-num {color: #bbb;}
858 table.cal td.odd p.day-num {color: #bbb;}
855 table.cal td.today {background:#ffffdd;}
859 table.cal td.today {background:#ffffdd;}
856 table.cal td.today p.day-num {font-weight: bold;}
860 table.cal td.today p.day-num {font-weight: bold;}
857 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
861 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
858 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
862 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
859 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
863 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
860 p.cal.legend span {display:block;}
864 p.cal.legend span {display:block;}
861
865
862 /***** Tooltips ******/
866 /***** Tooltips ******/
863 .tooltip{position:relative;z-index:24;}
867 .tooltip{position:relative;z-index:24;}
864 .tooltip:hover{z-index:25;color:#000;}
868 .tooltip:hover{z-index:25;color:#000;}
865 .tooltip span.tip{display: none; text-align:left;}
869 .tooltip span.tip{display: none; text-align:left;}
866
870
867 div.tooltip:hover span.tip{
871 div.tooltip:hover span.tip{
868 display:block;
872 display:block;
869 position:absolute;
873 position:absolute;
870 top:12px; width:270px;
874 top:12px; width:270px;
871 border:1px solid #555;
875 border:1px solid #555;
872 background-color:#fff;
876 background-color:#fff;
873 padding: 4px;
877 padding: 4px;
874 font-size: 0.8em;
878 font-size: 0.8em;
875 color:#505050;
879 color:#505050;
876 }
880 }
877
881
878 img.ui-datepicker-trigger {
882 img.ui-datepicker-trigger {
879 cursor: pointer;
883 cursor: pointer;
880 vertical-align: middle;
884 vertical-align: middle;
881 margin-left: 4px;
885 margin-left: 4px;
882 }
886 }
883
887
884 /***** Progress bar *****/
888 /***** Progress bar *****/
885 table.progress {
889 table.progress {
886 border-collapse: collapse;
890 border-collapse: collapse;
887 border-spacing: 0pt;
891 border-spacing: 0pt;
888 empty-cells: show;
892 empty-cells: show;
889 text-align: center;
893 text-align: center;
890 float:left;
894 float:left;
891 margin: 1px 6px 1px 0px;
895 margin: 1px 6px 1px 0px;
892 }
896 }
893
897
894 table.progress {width:80px;}
898 table.progress {width:80px;}
895 table.progress td { height: 1em; }
899 table.progress td { height: 1em; }
896 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
900 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
897 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
901 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
898 table.progress td.todo { background: #eee none repeat scroll 0%; }
902 table.progress td.todo { background: #eee none repeat scroll 0%; }
899 p.percent {font-size: 80%; margin:0;}
903 p.percent {font-size: 80%; margin:0;}
900 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
904 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
901
905
902 .version-overview table.progress {width:40em;}
906 .version-overview table.progress {width:40em;}
903 .version-overview table.progress td { height: 1.2em; }
907 .version-overview table.progress td { height: 1.2em; }
904
908
905 /***** Tabs *****/
909 /***** Tabs *****/
906 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
910 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
907 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
911 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
908 #content .tabs ul li {
912 #content .tabs ul li {
909 float:left;
913 float:left;
910 list-style-type:none;
914 list-style-type:none;
911 white-space:nowrap;
915 white-space:nowrap;
912 margin-right:4px;
916 margin-right:4px;
913 background:#fff;
917 background:#fff;
914 position:relative;
918 position:relative;
915 margin-bottom:-1px;
919 margin-bottom:-1px;
916 }
920 }
917 #content .tabs ul li a{
921 #content .tabs ul li a{
918 display:block;
922 display:block;
919 font-size: 0.9em;
923 font-size: 0.9em;
920 text-decoration:none;
924 text-decoration:none;
921 line-height:1.3em;
925 line-height:1.3em;
922 padding:4px 6px 4px 6px;
926 padding:4px 6px 4px 6px;
923 border: 1px solid #ccc;
927 border: 1px solid #ccc;
924 border-bottom: 1px solid #bbbbbb;
928 border-bottom: 1px solid #bbbbbb;
925 background-color: #f6f6f6;
929 background-color: #f6f6f6;
926 color:#999;
930 color:#999;
927 font-weight:bold;
931 font-weight:bold;
928 border-top-left-radius:3px;
932 border-top-left-radius:3px;
929 border-top-right-radius:3px;
933 border-top-right-radius:3px;
930 }
934 }
931
935
932 #content .tabs ul li a:hover {
936 #content .tabs ul li a:hover {
933 background-color: #ffffdd;
937 background-color: #ffffdd;
934 text-decoration:none;
938 text-decoration:none;
935 }
939 }
936
940
937 #content .tabs ul li a.selected {
941 #content .tabs ul li a.selected {
938 background-color: #fff;
942 background-color: #fff;
939 border: 1px solid #bbbbbb;
943 border: 1px solid #bbbbbb;
940 border-bottom: 1px solid #fff;
944 border-bottom: 1px solid #fff;
941 color:#444;
945 color:#444;
942 }
946 }
943
947
944 #content .tabs ul li a.selected:hover {background-color: #fff;}
948 #content .tabs ul li a.selected:hover {background-color: #fff;}
945
949
946 div.tabs-buttons { position:absolute; right: 0; width: 54px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
950 div.tabs-buttons { position:absolute; right: 0; width: 54px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
947
951
948 button.tab-left, button.tab-right {
952 button.tab-left, button.tab-right {
949 font-size: 0.9em;
953 font-size: 0.9em;
950 cursor: pointer;
954 cursor: pointer;
951 height:24px;
955 height:24px;
952 border: 1px solid #ccc;
956 border: 1px solid #ccc;
953 border-bottom: 1px solid #bbbbbb;
957 border-bottom: 1px solid #bbbbbb;
954 position:absolute;
958 position:absolute;
955 padding:4px;
959 padding:4px;
956 width: 20px;
960 width: 20px;
957 bottom: -1px;
961 bottom: -1px;
958 }
962 }
959 button.tab-left:hover, button.tab-right:hover {
963 button.tab-left:hover, button.tab-right:hover {
960 background-color: #f5f5f5;
964 background-color: #f5f5f5;
961 }
965 }
962 button.tab-left:focus, button.tab-right:focus {
966 button.tab-left:focus, button.tab-right:focus {
963 outline: 0;
967 outline: 0;
964 }
968 }
965
969
966 button.tab-left {
970 button.tab-left {
967 right: 20px;
971 right: 20px;
968 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
972 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
969 border-top-left-radius:3px;
973 border-top-left-radius:3px;
970 }
974 }
971
975
972 button.tab-right {
976 button.tab-right {
973 right: 0;
977 right: 0;
974 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
978 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
975 border-top-right-radius:3px;
979 border-top-right-radius:3px;
976 }
980 }
977
981
978 button.tab-left.disabled, button.tab-right.disabled {
982 button.tab-left.disabled, button.tab-right.disabled {
979 background-color: #ccc;
983 background-color: #ccc;
980 cursor: unset;
984 cursor: unset;
981 }
985 }
982
986
983 /***** Diff *****/
987 /***** Diff *****/
984 .diff_out { background: #fcc; }
988 .diff_out { background: #fcc; }
985 .diff_out span { background: #faa; }
989 .diff_out span { background: #faa; }
986 .diff_in { background: #cfc; }
990 .diff_in { background: #cfc; }
987 .diff_in span { background: #afa; }
991 .diff_in span { background: #afa; }
988
992
989 .text-diff {
993 .text-diff {
990 padding: 1em;
994 padding: 1em;
991 background-color:#f6f6f6;
995 background-color:#f6f6f6;
992 color:#505050;
996 color:#505050;
993 border: 1px solid #e4e4e4;
997 border: 1px solid #e4e4e4;
994 }
998 }
995
999
996 /***** Wiki *****/
1000 /***** Wiki *****/
997 div.wiki table {
1001 div.wiki table {
998 border-collapse: collapse;
1002 border-collapse: collapse;
999 margin-bottom: 1em;
1003 margin-bottom: 1em;
1000 }
1004 }
1001
1005
1002 div.wiki table, div.wiki td, div.wiki th {
1006 div.wiki table, div.wiki td, div.wiki th {
1003 border: 1px solid #bbb;
1007 border: 1px solid #bbb;
1004 padding: 4px;
1008 padding: 4px;
1005 }
1009 }
1006
1010
1007 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
1011 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
1008
1012
1009 div.wiki .external {
1013 div.wiki .external {
1010 background-position: 0% 60%;
1014 background-position: 0% 60%;
1011 background-repeat: no-repeat;
1015 background-repeat: no-repeat;
1012 padding-left: 12px;
1016 padding-left: 12px;
1013 background-image: url(../images/external.png);
1017 background-image: url(../images/external.png);
1014 }
1018 }
1015
1019
1016 div.wiki a {word-wrap: break-word;}
1020 div.wiki a {word-wrap: break-word;}
1017 div.wiki a.new {color: #b73535;}
1021 div.wiki a.new {color: #b73535;}
1018
1022
1019 div.wiki ul, div.wiki ol {margin-bottom:1em;}
1023 div.wiki ul, div.wiki ol {margin-bottom:1em;}
1020 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
1024 div.wiki li>ul, div.wiki li>ol {margin-bottom: 0;}
1021
1025
1022 div.wiki pre {
1026 div.wiki pre {
1023 margin: 1em 1em 1em 1.6em;
1027 margin: 1em 1em 1em 1.6em;
1024 padding: 8px;
1028 padding: 8px;
1025 background-color: #fafafa;
1029 background-color: #fafafa;
1026 border: 1px solid #e2e2e2;
1030 border: 1px solid #e2e2e2;
1027 border-radius: 3px;
1031 border-radius: 3px;
1028 width:auto;
1032 width:auto;
1029 overflow-x: auto;
1033 overflow-x: auto;
1030 overflow-y: hidden;
1034 overflow-y: hidden;
1031 }
1035 }
1032
1036
1033 div.wiki ul.toc {
1037 div.wiki ul.toc {
1034 background-color: #ffffdd;
1038 background-color: #ffffdd;
1035 border: 1px solid #e4e4e4;
1039 border: 1px solid #e4e4e4;
1036 padding: 4px;
1040 padding: 4px;
1037 line-height: 1.2em;
1041 line-height: 1.2em;
1038 margin-bottom: 12px;
1042 margin-bottom: 12px;
1039 margin-right: 12px;
1043 margin-right: 12px;
1040 margin-left: 0;
1044 margin-left: 0;
1041 display: table
1045 display: table
1042 }
1046 }
1043 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
1047 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
1044
1048
1045 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
1049 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
1046 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
1050 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
1047 div.wiki ul.toc ul { margin: 0; padding: 0; }
1051 div.wiki ul.toc ul { margin: 0; padding: 0; }
1048 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
1052 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
1049 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
1053 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
1050 div.wiki ul.toc a {
1054 div.wiki ul.toc a {
1051 font-size: 0.9em;
1055 font-size: 0.9em;
1052 font-weight: normal;
1056 font-weight: normal;
1053 text-decoration: none;
1057 text-decoration: none;
1054 color: #606060;
1058 color: #606060;
1055 }
1059 }
1056 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
1060 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
1057
1061
1058 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
1062 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
1059 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1063 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
1060 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1064 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
1061
1065
1062 div.wiki img {vertical-align:middle; max-width:100%;}
1066 div.wiki img {vertical-align:middle; max-width:100%;}
1063
1067
1064 /***** My page layout *****/
1068 /***** My page layout *****/
1065 .block-receiver {
1069 .block-receiver {
1066 border:1px dashed #c0c0c0;
1070 border:1px dashed #c0c0c0;
1067 margin-bottom: 20px;
1071 margin-bottom: 20px;
1068 padding: 15px 0 15px 0;
1072 padding: 15px 0 15px 0;
1069 }
1073 }
1070
1074
1071 .mypage-box {
1075 .mypage-box {
1072 margin:0 0 20px 0;
1076 margin:0 0 20px 0;
1073 color:#505050;
1077 color:#505050;
1074 line-height:1.5em;
1078 line-height:1.5em;
1075 }
1079 }
1076
1080
1077 .handle {cursor: move;}
1081 .handle {cursor: move;}
1078
1082
1079 a.close-icon {
1083 a.close-icon {
1080 display:block;
1084 display:block;
1081 margin-top:3px;
1085 margin-top:3px;
1082 overflow:hidden;
1086 overflow:hidden;
1083 width:12px;
1087 width:12px;
1084 height:12px;
1088 height:12px;
1085 background-repeat: no-repeat;
1089 background-repeat: no-repeat;
1086 cursor:pointer;
1090 cursor:pointer;
1087 background-image:url('../images/close.png');
1091 background-image:url('../images/close.png');
1088 }
1092 }
1089 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1093 a.close-icon:hover {background-image:url('../images/close_hl.png');}
1090
1094
1091 /***** Gantt chart *****/
1095 /***** Gantt chart *****/
1092 .gantt_hdr {
1096 .gantt_hdr {
1093 position:absolute;
1097 position:absolute;
1094 top:0;
1098 top:0;
1095 height:16px;
1099 height:16px;
1096 border-top: 1px solid #c0c0c0;
1100 border-top: 1px solid #c0c0c0;
1097 border-bottom: 1px solid #c0c0c0;
1101 border-bottom: 1px solid #c0c0c0;
1098 border-right: 1px solid #c0c0c0;
1102 border-right: 1px solid #c0c0c0;
1099 text-align: center;
1103 text-align: center;
1100 overflow: hidden;
1104 overflow: hidden;
1101 }
1105 }
1102
1106
1103 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1107 .gantt_hdr.nwday {background-color:#f1f1f1; color:#999;}
1104
1108
1105 .gantt_subjects { font-size: 0.8em; }
1109 .gantt_subjects { font-size: 0.8em; }
1106 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1110 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
1107
1111
1108 .task {
1112 .task {
1109 position: absolute;
1113 position: absolute;
1110 height:8px;
1114 height:8px;
1111 font-size:0.8em;
1115 font-size:0.8em;
1112 color:#888;
1116 color:#888;
1113 padding:0;
1117 padding:0;
1114 margin:0;
1118 margin:0;
1115 line-height:16px;
1119 line-height:16px;
1116 white-space:nowrap;
1120 white-space:nowrap;
1117 }
1121 }
1118
1122
1119 .task.label {width:100%;}
1123 .task.label {width:100%;}
1120 .task.label.project, .task.label.version { font-weight: bold; }
1124 .task.label.project, .task.label.version { font-weight: bold; }
1121
1125
1122 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1126 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
1123 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1127 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
1124 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1128 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
1125
1129
1126 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1130 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
1127 .task_late.parent, .task_done.parent { height: 3px;}
1131 .task_late.parent, .task_done.parent { height: 3px;}
1128 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1132 .task.parent.marker.starting { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; left: 0px; top: -1px;}
1129 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1133 .task.parent.marker.ending { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -4px; right: 0px; top: -1px;}
1130
1134
1131 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1135 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1132 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1136 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1133 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1137 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1134 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1138 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1135
1139
1136 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1140 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
1137 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1141 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
1138 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1142 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
1139 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1143 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
1140
1144
1141 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1145 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
1142 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1146 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
1143
1147
1144 /***** Icons *****/
1148 /***** Icons *****/
1145 .icon {
1149 .icon {
1146 background-position: 0% 50%;
1150 background-position: 0% 50%;
1147 background-repeat: no-repeat;
1151 background-repeat: no-repeat;
1148 padding-left: 20px;
1152 padding-left: 20px;
1149 padding-top: 2px;
1153 padding-top: 2px;
1150 padding-bottom: 3px;
1154 padding-bottom: 3px;
1151 }
1155 }
1152 .icon-only {
1156 .icon-only {
1153 background-position: 0% 50%;
1157 background-position: 0% 50%;
1154 background-repeat: no-repeat;
1158 background-repeat: no-repeat;
1155 padding-left: 16px;
1159 padding-left: 16px;
1156 display: inline-block;
1160 display: inline-block;
1157 width: 0;
1161 width: 0;
1158 height: 16px;
1162 height: 16px;
1159 overflow: hidden;
1163 overflow: hidden;
1160 padding-top: 0;
1164 padding-top: 0;
1161 padding-bottom: 0;
1165 padding-bottom: 0;
1162 font-size: 8px;
1166 font-size: 8px;
1163 vertical-align: text-bottom;
1167 vertical-align: middle;
1164 }
1168 }
1165 .icon-only::after {
1169 .icon-only::after {
1166 content: "&nbsp;";
1170 content: "&nbsp;";
1167 }
1171 }
1168
1172
1169 .icon-add { background-image: url(../images/add.png); }
1173 .icon-add { background-image: url(../images/add.png); }
1170 .icon-edit { background-image: url(../images/edit.png); }
1174 .icon-edit { background-image: url(../images/edit.png); }
1171 .icon-copy { background-image: url(../images/copy.png); }
1175 .icon-copy { background-image: url(../images/copy.png); }
1172 .icon-duplicate { background-image: url(../images/duplicate.png); }
1176 .icon-duplicate { background-image: url(../images/duplicate.png); }
1173 .icon-del { background-image: url(../images/delete.png); }
1177 .icon-del { background-image: url(../images/delete.png); }
1174 .icon-move { background-image: url(../images/move.png); }
1178 .icon-move { background-image: url(../images/move.png); }
1175 .icon-save { background-image: url(../images/save.png); }
1179 .icon-save { background-image: url(../images/save.png); }
1176 .icon-cancel { background-image: url(../images/cancel.png); }
1180 .icon-cancel { background-image: url(../images/cancel.png); }
1177 .icon-multiple { background-image: url(../images/table_multiple.png); }
1181 .icon-multiple { background-image: url(../images/table_multiple.png); }
1178 .icon-folder { background-image: url(../images/folder.png); }
1182 .icon-folder { background-image: url(../images/folder.png); }
1179 .open .icon-folder { background-image: url(../images/folder_open.png); }
1183 .open .icon-folder { background-image: url(../images/folder_open.png); }
1180 .icon-package { background-image: url(../images/package.png); }
1184 .icon-package { background-image: url(../images/package.png); }
1181 .icon-user { background-image: url(../images/user.png); }
1185 .icon-user { background-image: url(../images/user.png); }
1182 .icon-projects { background-image: url(../images/projects.png); }
1186 .icon-projects { background-image: url(../images/projects.png); }
1183 .icon-help { background-image: url(../images/help.png); }
1187 .icon-help { background-image: url(../images/help.png); }
1184 .icon-attachment { background-image: url(../images/attachment.png); }
1188 .icon-attachment { background-image: url(../images/attachment.png); }
1185 .icon-history { background-image: url(../images/history.png); }
1189 .icon-history { background-image: url(../images/history.png); }
1186 .icon-time { background-image: url(../images/time.png); }
1190 .icon-time { background-image: url(../images/time.png); }
1187 .icon-time-add { background-image: url(../images/time_add.png); }
1191 .icon-time-add { background-image: url(../images/time_add.png); }
1188 .icon-stats { background-image: url(../images/stats.png); }
1192 .icon-stats { background-image: url(../images/stats.png); }
1189 .icon-warning { background-image: url(../images/warning.png); }
1193 .icon-warning { background-image: url(../images/warning.png); }
1190 .icon-error { background-image: url(../images/exclamation.png); }
1194 .icon-error { background-image: url(../images/exclamation.png); }
1191 .icon-fav { background-image: url(../images/fav.png); }
1195 .icon-fav { background-image: url(../images/fav.png); }
1192 .icon-fav-off { background-image: url(../images/fav_off.png); }
1196 .icon-fav-off { background-image: url(../images/fav_off.png); }
1193 .icon-reload { background-image: url(../images/reload.png); }
1197 .icon-reload { background-image: url(../images/reload.png); }
1194 .icon-lock { background-image: url(../images/locked.png); }
1198 .icon-lock { background-image: url(../images/locked.png); }
1195 .icon-unlock { background-image: url(../images/unlock.png); }
1199 .icon-unlock { background-image: url(../images/unlock.png); }
1196 .icon-checked { background-image: url(../images/toggle_check.png); }
1200 .icon-checked { background-image: url(../images/toggle_check.png); }
1197 .icon-details { background-image: url(../images/zoom_in.png); }
1201 .icon-details { background-image: url(../images/zoom_in.png); }
1198 .icon-report { background-image: url(../images/report.png); }
1202 .icon-report { background-image: url(../images/report.png); }
1199 .icon-comment { background-image: url(../images/comment.png); }
1203 .icon-comment { background-image: url(../images/comment.png); }
1200 .icon-summary { background-image: url(../images/lightning.png); }
1204 .icon-summary { background-image: url(../images/lightning.png); }
1201 .icon-server-authentication { background-image: url(../images/server_key.png); }
1205 .icon-server-authentication { background-image: url(../images/server_key.png); }
1202 .icon-issue { background-image: url(../images/ticket.png); }
1206 .icon-issue { background-image: url(../images/ticket.png); }
1203 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1207 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
1204 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1208 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
1205 .icon-magnifier { background-image: url(../images/magnifier.png); }
1209 .icon-magnifier { background-image: url(../images/magnifier.png); }
1206 .icon-passwd { background-image: url(../images/textfield_key.png); }
1210 .icon-passwd { background-image: url(../images/textfield_key.png); }
1207 .icon-test { background-image: url(../images/bullet_go.png); }
1211 .icon-test { background-image: url(../images/bullet_go.png); }
1208 .icon-email { background-image: url(../images/email.png); }
1212 .icon-email { background-image: url(../images/email.png); }
1209 .icon-email-disabled { background-image: url(../images/email_disabled.png); }
1213 .icon-email-disabled { background-image: url(../images/email_disabled.png); }
1210 .icon-email-add { background-image: url(../images/email_add.png); }
1214 .icon-email-add { background-image: url(../images/email_add.png); }
1211 .icon-move-up { background-image: url(../images/1uparrow.png); }
1215 .icon-move-up { background-image: url(../images/1uparrow.png); }
1212 .icon-move-top { background-image: url(../images/2uparrow.png); }
1216 .icon-move-top { background-image: url(../images/2uparrow.png); }
1213 .icon-move-down { background-image: url(../images/1downarrow.png); }
1217 .icon-move-down { background-image: url(../images/1downarrow.png); }
1214 .icon-move-bottom { background-image: url(../images/2downarrow.png); }
1218 .icon-move-bottom { background-image: url(../images/2downarrow.png); }
1215 .icon-ok { background-image: url(../images/true.png); }
1219 .icon-ok { background-image: url(../images/true.png); }
1216 .icon-not-ok { background-image: url(../images/false.png); }
1220 .icon-not-ok { background-image: url(../images/false.png); }
1217 .icon-link-break { background-image: url(../images/link_break.png); }
1221 .icon-link-break { background-image: url(../images/link_break.png); }
1218 .icon-list { background-image: url(../images/text_list_bullets.png); }
1222 .icon-list { background-image: url(../images/text_list_bullets.png); }
1219
1223
1220 .icon-file { background-image: url(../images/files/default.png); }
1224 .icon-file { background-image: url(../images/files/default.png); }
1221 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1225 .icon-file.text-plain { background-image: url(../images/files/text.png); }
1222 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1226 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
1223 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1227 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
1224 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1228 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
1225 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1229 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
1226 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1230 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
1227 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1231 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
1228 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1232 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
1229 .icon-file.text-css { background-image: url(../images/files/css.png); }
1233 .icon-file.text-css { background-image: url(../images/files/css.png); }
1230 .icon-file.text-html { background-image: url(../images/files/html.png); }
1234 .icon-file.text-html { background-image: url(../images/files/html.png); }
1231 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1235 .icon-file.image-gif { background-image: url(../images/files/image.png); }
1232 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1236 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
1233 .icon-file.image-png { background-image: url(../images/files/image.png); }
1237 .icon-file.image-png { background-image: url(../images/files/image.png); }
1234 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1238 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
1235 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1239 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
1236 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1240 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
1237 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1241 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
1238
1242
1239 .sort-handle { width:16px; height:16px; background:url(../images/reorder.png) no-repeat 0 50%; cursor:move; }
1243 .sort-handle { width:16px; height:16px; background:url(../images/reorder.png) no-repeat 0 50%; cursor:move; }
1240 .sort-handle.ajax-loading { background-image: url(../images/loading.gif); }
1244 .sort-handle.ajax-loading { background-image: url(../images/loading.gif); }
1241 tr.ui-sortable-helper { border:1px solid #e4e4e4; }
1245 tr.ui-sortable-helper { border:1px solid #e4e4e4; }
1242
1246
1243 .contextual>.icon:not(:first-child), .buttons>.icon:not(:first-child) { margin-left: 5px; }
1247 .contextual>.icon:not(:first-child), .buttons>.icon:not(:first-child) { margin-left: 5px; }
1244
1248
1245 img.gravatar {
1249 img.gravatar {
1246 vertical-align: middle;
1250 vertical-align: middle;
1247 border-radius: 20%;
1251 border-radius: 20%;
1248 }
1252 }
1249
1253
1250 div.issue img.gravatar {
1254 div.issue img.gravatar {
1251 float: left;
1255 float: left;
1252 margin: 0 6px 0 0;
1256 margin: 0 6px 0 0;
1253 }
1257 }
1254
1258
1255 h2 img.gravatar {margin: -2px 4px -4px 0;}
1259 h2 img.gravatar {margin: -2px 4px -4px 0;}
1256 h3 img.gravatar {margin: -4px 4px -4px 0;}
1260 h3 img.gravatar {margin: -4px 4px -4px 0;}
1257 h4 img.gravatar {margin: -2px 4px -4px 0;}
1261 h4 img.gravatar {margin: -2px 4px -4px 0;}
1258 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1262 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1259 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1263 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1260 /* Used on 12px Gravatar img tags without the icon background */
1264 /* Used on 12px Gravatar img tags without the icon background */
1261 .icon-gravatar {float: left; margin-right: 4px;}
1265 .icon-gravatar {float: left; margin-right: 4px;}
1262
1266
1263 #activity dt, .journal {clear: left;}
1267 #activity dt, .journal {clear: left;}
1264
1268
1265 .journal-link {float: right;}
1269 .journal-link {float: right;}
1266
1270
1267 h2 img { vertical-align:middle; }
1271 h2 img { vertical-align:middle; }
1268
1272
1269 .hascontextmenu { cursor: context-menu; }
1273 .hascontextmenu { cursor: context-menu; }
1270
1274
1271 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1275 .sample-data {border:1px solid #ccc; border-collapse:collapse; background-color:#fff; margin:0.5em;}
1272 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1276 .sample-data td {border:1px solid #ccc; padding: 2px 4px; font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
1273 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1277 .sample-data tr:first-child td {font-weight:bold; text-align:center;}
1274
1278
1275 .ui-progressbar {position: relative;}
1279 .ui-progressbar {position: relative;}
1276 #progress-label {
1280 #progress-label {
1277 position: absolute; left: 50%; top: 4px;
1281 position: absolute; left: 50%; top: 4px;
1278 font-weight: bold;
1282 font-weight: bold;
1279 color: #555; text-shadow: 1px 1px 0 #fff;
1283 color: #555; text-shadow: 1px 1px 0 #fff;
1280 }
1284 }
1281
1285
1282 /* Custom JQuery styles */
1286 /* Custom JQuery styles */
1283 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1287 .ui-datepicker-title select {width:70px !important; margin-top:-2px !important; margin-right:4px !important;}
1284
1288
1285
1289
1286 /************* CodeRay styles *************/
1290 /************* CodeRay styles *************/
1287 .syntaxhl div {display: inline;}
1291 .syntaxhl div {display: inline;}
1288 .syntaxhl .code pre { overflow: auto }
1292 .syntaxhl .code pre { overflow: auto }
1289
1293
1290 .syntaxhl .annotation { color:#007 }
1294 .syntaxhl .annotation { color:#007 }
1291 .syntaxhl .attribute-name { color:#b48 }
1295 .syntaxhl .attribute-name { color:#b48 }
1292 .syntaxhl .attribute-value { color:#700 }
1296 .syntaxhl .attribute-value { color:#700 }
1293 .syntaxhl .binary { color:#549 }
1297 .syntaxhl .binary { color:#549 }
1294 .syntaxhl .binary .char { color:#325 }
1298 .syntaxhl .binary .char { color:#325 }
1295 .syntaxhl .binary .delimiter { color:#325 }
1299 .syntaxhl .binary .delimiter { color:#325 }
1296 .syntaxhl .char { color:#D20 }
1300 .syntaxhl .char { color:#D20 }
1297 .syntaxhl .char .content { color:#D20 }
1301 .syntaxhl .char .content { color:#D20 }
1298 .syntaxhl .char .delimiter { color:#710 }
1302 .syntaxhl .char .delimiter { color:#710 }
1299 .syntaxhl .class { color:#B06; font-weight:bold }
1303 .syntaxhl .class { color:#B06; font-weight:bold }
1300 .syntaxhl .class-variable { color:#369 }
1304 .syntaxhl .class-variable { color:#369 }
1301 .syntaxhl .color { color:#0A0 }
1305 .syntaxhl .color { color:#0A0 }
1302 .syntaxhl .comment { color:#777 }
1306 .syntaxhl .comment { color:#777 }
1303 .syntaxhl .comment .char { color:#444 }
1307 .syntaxhl .comment .char { color:#444 }
1304 .syntaxhl .comment .delimiter { color:#444 }
1308 .syntaxhl .comment .delimiter { color:#444 }
1305 .syntaxhl .constant { color:#036; font-weight:bold }
1309 .syntaxhl .constant { color:#036; font-weight:bold }
1306 .syntaxhl .decorator { color:#B0B }
1310 .syntaxhl .decorator { color:#B0B }
1307 .syntaxhl .definition { color:#099; font-weight:bold }
1311 .syntaxhl .definition { color:#099; font-weight:bold }
1308 .syntaxhl .delimiter { color:black }
1312 .syntaxhl .delimiter { color:black }
1309 .syntaxhl .directive { color:#088; font-weight:bold }
1313 .syntaxhl .directive { color:#088; font-weight:bold }
1310 .syntaxhl .docstring { color:#D42; }
1314 .syntaxhl .docstring { color:#D42; }
1311 .syntaxhl .doctype { color:#34b }
1315 .syntaxhl .doctype { color:#34b }
1312 .syntaxhl .done { text-decoration: line-through; color: gray }
1316 .syntaxhl .done { text-decoration: line-through; color: gray }
1313 .syntaxhl .entity { color:#800; font-weight:bold }
1317 .syntaxhl .entity { color:#800; font-weight:bold }
1314 .syntaxhl .error { color:#F00; background-color:#FAA }
1318 .syntaxhl .error { color:#F00; background-color:#FAA }
1315 .syntaxhl .escape { color:#666 }
1319 .syntaxhl .escape { color:#666 }
1316 .syntaxhl .exception { color:#C00; font-weight:bold }
1320 .syntaxhl .exception { color:#C00; font-weight:bold }
1317 .syntaxhl .float { color:#60E }
1321 .syntaxhl .float { color:#60E }
1318 .syntaxhl .function { color:#06B; font-weight:bold }
1322 .syntaxhl .function { color:#06B; font-weight:bold }
1319 .syntaxhl .function .delimiter { color:#059 }
1323 .syntaxhl .function .delimiter { color:#059 }
1320 .syntaxhl .function .content { color:#037 }
1324 .syntaxhl .function .content { color:#037 }
1321 .syntaxhl .global-variable { color:#d70 }
1325 .syntaxhl .global-variable { color:#d70 }
1322 .syntaxhl .hex { color:#02b }
1326 .syntaxhl .hex { color:#02b }
1323 .syntaxhl .id { color:#33D; font-weight:bold }
1327 .syntaxhl .id { color:#33D; font-weight:bold }
1324 .syntaxhl .include { color:#B44; font-weight:bold }
1328 .syntaxhl .include { color:#B44; font-weight:bold }
1325 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1329 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1326 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1330 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1327 .syntaxhl .instance-variable { color:#33B }
1331 .syntaxhl .instance-variable { color:#33B }
1328 .syntaxhl .integer { color:#00D }
1332 .syntaxhl .integer { color:#00D }
1329 .syntaxhl .imaginary { color:#f00 }
1333 .syntaxhl .imaginary { color:#f00 }
1330 .syntaxhl .important { color:#D00 }
1334 .syntaxhl .important { color:#D00 }
1331 .syntaxhl .key { color: #606 }
1335 .syntaxhl .key { color: #606 }
1332 .syntaxhl .key .char { color: #60f }
1336 .syntaxhl .key .char { color: #60f }
1333 .syntaxhl .key .delimiter { color: #404 }
1337 .syntaxhl .key .delimiter { color: #404 }
1334 .syntaxhl .keyword { color:#080; font-weight:bold }
1338 .syntaxhl .keyword { color:#080; font-weight:bold }
1335 .syntaxhl .label { color:#970; font-weight:bold }
1339 .syntaxhl .label { color:#970; font-weight:bold }
1336 .syntaxhl .local-variable { color:#950 }
1340 .syntaxhl .local-variable { color:#950 }
1337 .syntaxhl .map .content { color:#808 }
1341 .syntaxhl .map .content { color:#808 }
1338 .syntaxhl .map .delimiter { color:#40A}
1342 .syntaxhl .map .delimiter { color:#40A}
1339 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1343 .syntaxhl .map { background-color:hsla(200,100%,50%,0.06); }
1340 .syntaxhl .namespace { color:#707; font-weight:bold }
1344 .syntaxhl .namespace { color:#707; font-weight:bold }
1341 .syntaxhl .octal { color:#40E }
1345 .syntaxhl .octal { color:#40E }
1342 .syntaxhl .operator { }
1346 .syntaxhl .operator { }
1343 .syntaxhl .predefined { color:#369; font-weight:bold }
1347 .syntaxhl .predefined { color:#369; font-weight:bold }
1344 .syntaxhl .predefined-constant { color:#069 }
1348 .syntaxhl .predefined-constant { color:#069 }
1345 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1349 .syntaxhl .predefined-type { color:#0a8; font-weight:bold }
1346 .syntaxhl .preprocessor { color:#579 }
1350 .syntaxhl .preprocessor { color:#579 }
1347 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1351 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1348 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1352 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1349 .syntaxhl .regexp .content { color:#808 }
1353 .syntaxhl .regexp .content { color:#808 }
1350 .syntaxhl .regexp .delimiter { color:#404 }
1354 .syntaxhl .regexp .delimiter { color:#404 }
1351 .syntaxhl .regexp .modifier { color:#C2C }
1355 .syntaxhl .regexp .modifier { color:#C2C }
1352 .syntaxhl .reserved { color:#080; font-weight:bold }
1356 .syntaxhl .reserved { color:#080; font-weight:bold }
1353 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1357 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1354 .syntaxhl .shell .content { color:#2B2 }
1358 .syntaxhl .shell .content { color:#2B2 }
1355 .syntaxhl .shell .delimiter { color:#161 }
1359 .syntaxhl .shell .delimiter { color:#161 }
1356 .syntaxhl .string { background-color:hsla(0,100%,50%,0.05); }
1360 .syntaxhl .string { background-color:hsla(0,100%,50%,0.05); }
1357 .syntaxhl .string .char { color: #b0b }
1361 .syntaxhl .string .char { color: #b0b }
1358 .syntaxhl .string .content { color: #D20 }
1362 .syntaxhl .string .content { color: #D20 }
1359 .syntaxhl .string .delimiter { color: #710 }
1363 .syntaxhl .string .delimiter { color: #710 }
1360 .syntaxhl .string .modifier { color: #E40 }
1364 .syntaxhl .string .modifier { color: #E40 }
1361 .syntaxhl .symbol { color:#A60 }
1365 .syntaxhl .symbol { color:#A60 }
1362 .syntaxhl .symbol .content { color:#A60 }
1366 .syntaxhl .symbol .content { color:#A60 }
1363 .syntaxhl .symbol .delimiter { color:#740 }
1367 .syntaxhl .symbol .delimiter { color:#740 }
1364 .syntaxhl .tag { color:#070; font-weight:bold }
1368 .syntaxhl .tag { color:#070; font-weight:bold }
1365 .syntaxhl .type { color:#339; font-weight:bold }
1369 .syntaxhl .type { color:#339; font-weight:bold }
1366 .syntaxhl .value { color: #088 }
1370 .syntaxhl .value { color: #088 }
1367 .syntaxhl .variable { color:#037 }
1371 .syntaxhl .variable { color:#037 }
1368
1372
1369 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1373 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1370 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1374 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1371 .syntaxhl .change { color: #bbf; background: #007 }
1375 .syntaxhl .change { color: #bbf; background: #007 }
1372 .syntaxhl .head { color: #f8f; background: #505 }
1376 .syntaxhl .head { color: #f8f; background: #505 }
1373 .syntaxhl .head .filename { color: white; }
1377 .syntaxhl .head .filename { color: white; }
1374
1378
1375 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1379 .syntaxhl .delete .eyecatcher { background-color: hsla(0,100%,50%,0.2); border: 1px solid hsla(0,100%,45%,0.5); margin: -1px; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; }
1376 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1380 .syntaxhl .insert .eyecatcher { background-color: hsla(120,100%,50%,0.2); border: 1px solid hsla(120,100%,25%,0.5); margin: -1px; border-top: none; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; }
1377
1381
1378 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1382 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1379 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1383 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1380 .syntaxhl .change .change { color: #88f }
1384 .syntaxhl .change .change { color: #88f }
1381 .syntaxhl .head .head { color: #f4f }
1385 .syntaxhl .head .head { color: #f4f }
1382
1386
1383 /***** Media print specific styles *****/
1387 /***** Media print specific styles *****/
1384 @media print {
1388 @media print {
1385 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1389 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1386 #main { background: #fff; }
1390 #main { background: #fff; }
1387 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1391 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1388 #wiki_add_attachment { display:none; }
1392 #wiki_add_attachment { display:none; }
1389 .hide-when-print, .pagination ul.pages, .pagination .per-page { display: none !important; }
1393 .hide-when-print, .pagination ul.pages, .pagination .per-page { display: none !important; }
1390 .autoscroll {overflow-x: visible;}
1394 .autoscroll {overflow-x: visible;}
1391 table.list {margin-top:0.5em;}
1395 table.list {margin-top:0.5em;}
1392 table.list th, table.list td {border: 1px solid #aaa;}
1396 table.list th, table.list td {border: 1px solid #aaa;}
1393 }
1397 }
1394
1398
1395 /* Accessibility specific styles */
1399 /* Accessibility specific styles */
1396 .hidden-for-sighted {
1400 .hidden-for-sighted {
1397 position:absolute;
1401 position:absolute;
1398 left:-10000px;
1402 left:-10000px;
1399 top:auto;
1403 top:auto;
1400 width:1px;
1404 width:1px;
1401 height:1px;
1405 height:1px;
1402 overflow:hidden;
1406 overflow:hidden;
1403 }
1407 }
General Comments 0
You need to be logged in to leave comments. Login now