The requested changes are too big and content was truncated. Show full diff
@@ -0,0 +1,1 | |||
|
1 | $('#attachments_<%= j params[:attachment_id] %>').remove(); |
@@ -0,0 +1,9 | |||
|
1 | var fileSpan = $('#attachments_<%= j params[:attachment_id] %>'); | |
|
2 | $('<input>', { type: 'hidden', name: 'attachments[<%= j params[:attachment_id] %>][token]' } ).val('<%= j @attachment.token %>').appendTo(fileSpan); | |
|
3 | fileSpan.find('a.remove-upload') | |
|
4 | .attr({ | |
|
5 | "data-remote": true, | |
|
6 | "data-method": 'delete', | |
|
7 | href: '<%= j attachment_path(@attachment, :attachment_id => params[:attachment_id], :format => 'js') %>' | |
|
8 | }) | |
|
9 | .off('click'); |
|
1 | NO CONTENT: new file 100644, binary diff hidden |
@@ -0,0 +1,189 | |||
|
1 | /* Redmine - project management software | |
|
2 | Copyright (C) 2006-2012 Jean-Philippe Lang */ | |
|
3 | ||
|
4 | function addFile(inputEl, file, eagerUpload) { | |
|
5 | ||
|
6 | if ($('#attachments_fields').children().length < 10) { | |
|
7 | ||
|
8 | var attachmentId = addFile.nextAttachmentId++; | |
|
9 | ||
|
10 | var fileSpan = $('<span>', { id: 'attachments_' + attachmentId }); | |
|
11 | ||
|
12 | fileSpan.append( | |
|
13 | $('<input>', { type: 'text', 'class': 'filename readonly', name: 'attachments[' + 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), | |
|
15 | $('<a> </a>').attr({ href: "#", 'class': 'remove-upload' }).click(removeFile).toggle(!eagerUpload) | |
|
16 | ).appendTo('#attachments_fields'); | |
|
17 | ||
|
18 | if(eagerUpload) { | |
|
19 | ajaxUpload(file, attachmentId, fileSpan, inputEl); | |
|
20 | } | |
|
21 | ||
|
22 | return attachmentId; | |
|
23 | } | |
|
24 | return null; | |
|
25 | } | |
|
26 | ||
|
27 | addFile.nextAttachmentId = 1; | |
|
28 | ||
|
29 | function ajaxUpload(file, attachmentId, fileSpan, inputEl) { | |
|
30 | ||
|
31 | function onLoadstart(e) { | |
|
32 | fileSpan.removeClass('ajax-waiting'); | |
|
33 | fileSpan.addClass('ajax-loading'); | |
|
34 | $('input:submit', $(this).parents('form')).attr('disabled', 'disabled'); | |
|
35 | } | |
|
36 | ||
|
37 | function onProgress(e) { | |
|
38 | if(e.lengthComputable) { | |
|
39 | this.progressbar( 'value', e.loaded * 100 / e.total ); | |
|
40 | } | |
|
41 | } | |
|
42 | ||
|
43 | function actualUpload(file, attachmentId, fileSpan, inputEl) { | |
|
44 | ||
|
45 | ajaxUpload.uploading++; | |
|
46 | ||
|
47 | uploadBlob(file, $(inputEl).data('upload-path'), attachmentId, { | |
|
48 | loadstartEventHandler: onLoadstart.bind(progressSpan), | |
|
49 | progressEventHandler: onProgress.bind(progressSpan) | |
|
50 | }) | |
|
51 | .done(function(result) { | |
|
52 | progressSpan.progressbar( 'value', 100 ).remove(); | |
|
53 | fileSpan.find('input.description, a').css('display', 'inline-block'); | |
|
54 | }) | |
|
55 | .fail(function(result) { | |
|
56 | progressSpan.text(result.statusText); | |
|
57 | }).always(function() { | |
|
58 | ajaxUpload.uploading--; | |
|
59 | fileSpan.removeClass('ajax-loading'); | |
|
60 | var form = fileSpan.parents('form'); | |
|
61 | if (form.queue('upload').length == 0 && ajaxUpload.uploading == 0) { | |
|
62 | $('input:submit', form).removeAttr('disabled'); | |
|
63 | } | |
|
64 | form.dequeue('upload'); | |
|
65 | }); | |
|
66 | } | |
|
67 | ||
|
68 | var progressSpan = $('<div>').insertAfter(fileSpan.find('input.filename')); | |
|
69 | progressSpan.progressbar(); | |
|
70 | fileSpan.addClass('ajax-waiting'); | |
|
71 | ||
|
72 | var maxSyncUpload = $(inputEl).data('max-concurrent-uploads'); | |
|
73 | ||
|
74 | if(maxSyncUpload == null || maxSyncUpload <= 0 || ajaxUpload.uploading < maxSyncUpload) | |
|
75 | actualUpload(file, attachmentId, fileSpan, inputEl); | |
|
76 | else | |
|
77 | $(inputEl).parents('form').queue('upload', actualUpload.bind(this, file, attachmentId, fileSpan, inputEl)); | |
|
78 | } | |
|
79 | ||
|
80 | ajaxUpload.uploading = 0; | |
|
81 | ||
|
82 | function removeFile() { | |
|
83 | $(this).parent('span').remove(); | |
|
84 | return false; | |
|
85 | } | |
|
86 | ||
|
87 | function uploadBlob(blob, uploadUrl, attachmentId, options) { | |
|
88 | ||
|
89 | var actualOptions = $.extend({ | |
|
90 | loadstartEventHandler: $.noop, | |
|
91 | progressEventHandler: $.noop | |
|
92 | }, options); | |
|
93 | ||
|
94 | uploadUrl = uploadUrl + '?attachment_id=' + attachmentId; | |
|
95 | if (blob instanceof window.File) { | |
|
96 | uploadUrl += '&filename=' + encodeURIComponent(blob.name); | |
|
97 | } | |
|
98 | ||
|
99 | return $.ajax(uploadUrl, { | |
|
100 | type: 'POST', | |
|
101 | contentType: 'application/octet-stream', | |
|
102 | beforeSend: function(jqXhr) { | |
|
103 | jqXhr.setRequestHeader('Accept', 'application/js'); | |
|
104 | }, | |
|
105 | xhr: function() { | |
|
106 | var xhr = $.ajaxSettings.xhr(); | |
|
107 | xhr.upload.onloadstart = actualOptions.loadstartEventHandler; | |
|
108 | xhr.upload.onprogress = actualOptions.progressEventHandler; | |
|
109 | return xhr; | |
|
110 | }, | |
|
111 | data: blob, | |
|
112 | cache: false, | |
|
113 | processData: false | |
|
114 | }); | |
|
115 | } | |
|
116 | ||
|
117 | function addInputFiles(inputEl) { | |
|
118 | var clearedFileInput = $(inputEl).clone().val(''); | |
|
119 | ||
|
120 | if (inputEl.files) { | |
|
121 | // upload files using ajax | |
|
122 | uploadAndAttachFiles(inputEl.files, inputEl); | |
|
123 | $(inputEl).remove(); | |
|
124 | } else { | |
|
125 | // browser not supporting the file API, upload on form submission | |
|
126 | var attachmentId; | |
|
127 | var aFilename = inputEl.value.split(/\/|\\/); | |
|
128 | attachmentId = addFile(inputEl, { name: aFilename[ aFilename.length - 1 ] }, false); | |
|
129 | if (attachmentId) { | |
|
130 | $(inputEl).attr({ name: 'attachments[' + attachmentId + '][file]', style: 'display:none;' }).appendTo('#attachments_' + attachmentId); | |
|
131 | } | |
|
132 | } | |
|
133 | ||
|
134 | clearedFileInput.insertAfter('#attachments_fields'); | |
|
135 | } | |
|
136 | ||
|
137 | function uploadAndAttachFiles(files, inputEl) { | |
|
138 | ||
|
139 | var maxFileSize = $(inputEl).data('max-file-size'); | |
|
140 | var maxFileSizeExceeded = $(inputEl).data('max-file-size-message'); | |
|
141 | ||
|
142 | var sizeExceeded = false; | |
|
143 | $.each(files, function() { | |
|
144 | if (this.size && maxFileSize && this.size > parseInt(maxFileSize)) {sizeExceeded=true;} | |
|
145 | }); | |
|
146 | if (sizeExceeded) { | |
|
147 | window.alert(maxFileSizeExceeded); | |
|
148 | } else { | |
|
149 | $.each(files, function() {addFile(inputEl, this, true);}); | |
|
150 | } | |
|
151 | } | |
|
152 | ||
|
153 | function handleFileDropEvent(e) { | |
|
154 | ||
|
155 | $(this).removeClass('fileover'); | |
|
156 | blockEventPropagation(e); | |
|
157 | ||
|
158 | if ($.inArray('Files', e.dataTransfer.types) > -1) { | |
|
159 | ||
|
160 | uploadAndAttachFiles(e.dataTransfer.files, $('input:file[name=attachments_files]')); | |
|
161 | } | |
|
162 | } | |
|
163 | ||
|
164 | function dragOverHandler(e) { | |
|
165 | $(this).addClass('fileover'); | |
|
166 | blockEventPropagation(e); | |
|
167 | } | |
|
168 | ||
|
169 | function dragOutHandler(e) { | |
|
170 | $(this).removeClass('fileover'); | |
|
171 | blockEventPropagation(e); | |
|
172 | } | |
|
173 | ||
|
174 | function setupFileDrop() { | |
|
175 | if (window.File && window.FileList && window.ProgressEvent && window.FormData) { | |
|
176 | ||
|
177 | $.event.fixHooks.drop = { props: [ 'dataTransfer' ] }; | |
|
178 | ||
|
179 | $('form div.box').has('input:file').each(function() { | |
|
180 | $(this).on({ | |
|
181 | dragover: dragOverHandler, | |
|
182 | dragleave: dragOutHandler, | |
|
183 | drop: handleFileDropEvent | |
|
184 | }); | |
|
185 | }); | |
|
186 | } | |
|
187 | } | |
|
188 | ||
|
189 | $(document).ready(setupFileDrop); |
@@ -0,0 +1,132 | |||
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2012 Jean-Philippe Lang | |
|
3 | # | |
|
4 | # This program is free software; you can redistribute it and/or | |
|
5 | # modify it under the terms of the GNU General Public License | |
|
6 | # as published by the Free Software Foundation; either version 2 | |
|
7 | # of the License, or (at your option) any later version. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU General Public License | |
|
15 | # along with this program; if not, write to the Free Software | |
|
16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |
|
17 | ||
|
18 | require File.expand_path('../../test_helper', __FILE__) | |
|
19 | ||
|
20 | class AttachmentsTest < ActionController::IntegrationTest | |
|
21 | fixtures :projects, :enabled_modules, | |
|
22 | :users, :roles, :members, :member_roles, | |
|
23 | :trackers, :projects_trackers, | |
|
24 | :issue_statuses, :enumerations | |
|
25 | ||
|
26 | def test_upload_as_js_and_attach_to_an_issue | |
|
27 | log_user('jsmith', 'jsmith') | |
|
28 | ||
|
29 | token = ajax_upload('myupload.txt', 'File content') | |
|
30 | ||
|
31 | assert_difference 'Issue.count' do | |
|
32 | post '/projects/ecookbook/issues', { | |
|
33 | :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, | |
|
34 | :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} | |
|
35 | } | |
|
36 | assert_response 302 | |
|
37 | end | |
|
38 | ||
|
39 | issue = Issue.order('id DESC').first | |
|
40 | assert_equal 'Issue with upload', issue.subject | |
|
41 | assert_equal 1, issue.attachments.count | |
|
42 | ||
|
43 | attachment = issue.attachments.first | |
|
44 | assert_equal 'myupload.txt', attachment.filename | |
|
45 | assert_equal 'My uploaded file', attachment.description | |
|
46 | assert_equal 'File content'.length, attachment.filesize | |
|
47 | end | |
|
48 | ||
|
49 | def test_upload_as_js_and_preview_as_inline_attachment | |
|
50 | log_user('jsmith', 'jsmith') | |
|
51 | ||
|
52 | token = ajax_upload('myupload.jpg', 'JPEG content') | |
|
53 | ||
|
54 | post '/issues/preview/new/ecookbook', { | |
|
55 | :issue => {:tracker_id => 1, :description => 'Inline upload: !myupload.jpg!'}, | |
|
56 | :attachments => {'1' => {:filename => 'myupload.jpg', :description => 'My uploaded file', :token => token}} | |
|
57 | } | |
|
58 | assert_response :success | |
|
59 | ||
|
60 | attachment_path = response.body.match(%r{<img src="(/attachments/download/\d+)"})[1] | |
|
61 | assert_not_nil token, "No attachment path found in response:\n#{response.body}" | |
|
62 | ||
|
63 | get attachment_path | |
|
64 | assert_response :success | |
|
65 | assert_equal 'JPEG content', response.body | |
|
66 | end | |
|
67 | ||
|
68 | def test_upload_and_resubmit_after_validation_failure | |
|
69 | log_user('jsmith', 'jsmith') | |
|
70 | ||
|
71 | token = ajax_upload('myupload.txt', 'File content') | |
|
72 | ||
|
73 | assert_no_difference 'Issue.count' do | |
|
74 | post '/projects/ecookbook/issues', { | |
|
75 | :issue => {:tracker_id => 1, :subject => ''}, | |
|
76 | :attachments => {'1' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} | |
|
77 | } | |
|
78 | assert_response :success | |
|
79 | end | |
|
80 | assert_select 'input[type=hidden][name=?][value=?]', 'attachments[p0][token]', token | |
|
81 | assert_select 'input[name=?][value=?]', 'attachments[p0][filename]', 'myupload.txt' | |
|
82 | assert_select 'input[name=?][value=?]', 'attachments[p0][description]', 'My uploaded file' | |
|
83 | ||
|
84 | assert_difference 'Issue.count' do | |
|
85 | post '/projects/ecookbook/issues', { | |
|
86 | :issue => {:tracker_id => 1, :subject => 'Issue with upload'}, | |
|
87 | :attachments => {'p0' => {:filename => 'myupload.txt', :description => 'My uploaded file', :token => token}} | |
|
88 | } | |
|
89 | assert_response 302 | |
|
90 | end | |
|
91 | ||
|
92 | issue = Issue.order('id DESC').first | |
|
93 | assert_equal 'Issue with upload', issue.subject | |
|
94 | assert_equal 1, issue.attachments.count | |
|
95 | ||
|
96 | attachment = issue.attachments.first | |
|
97 | assert_equal 'myupload.txt', attachment.filename | |
|
98 | assert_equal 'My uploaded file', attachment.description | |
|
99 | assert_equal 'File content'.length, attachment.filesize | |
|
100 | end | |
|
101 | ||
|
102 | def test_upload_as_js_and_destroy | |
|
103 | log_user('jsmith', 'jsmith') | |
|
104 | ||
|
105 | token = ajax_upload('myupload.txt', 'File content') | |
|
106 | ||
|
107 | attachment = Attachment.order('id DESC').first | |
|
108 | attachment_path = "/attachments/#{attachment.id}.js?attachment_id=1" | |
|
109 | assert_include "href: '#{attachment_path}'", response.body, "Path to attachment: #{attachment_path} not found in response:\n#{response.body}" | |
|
110 | ||
|
111 | assert_difference 'Attachment.count', -1 do | |
|
112 | delete attachment_path | |
|
113 | assert_response :success | |
|
114 | end | |
|
115 | ||
|
116 | assert_include "$('#attachments_1').remove();", response.body | |
|
117 | end | |
|
118 | ||
|
119 | private | |
|
120 | ||
|
121 | def ajax_upload(filename, content, attachment_id=1) | |
|
122 | assert_difference 'Attachment.count' do | |
|
123 | post "/uploads.js?attachment_id=#{attachment_id}&filename=#{filename}", content, {"CONTENT_TYPE" => 'application/octet-stream'} | |
|
124 | assert_response :success | |
|
125 | assert_equal 'text/javascript', response.content_type | |
|
126 | end | |
|
127 | ||
|
128 | token = response.body.match(/\.val\('(\d+\.[0-9a-f]+)'\)/)[1] | |
|
129 | assert_not_nil token, "No upload token found in response:\n#{response.body}" | |
|
130 | token | |
|
131 | end | |
|
132 | end |
@@ -1,590 +1,600 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require 'uri' |
|
19 | 19 | require 'cgi' |
|
20 | 20 | |
|
21 | 21 | class Unauthorized < Exception; end |
|
22 | 22 | |
|
23 | 23 | class ApplicationController < ActionController::Base |
|
24 | 24 | include Redmine::I18n |
|
25 | 25 | |
|
26 | 26 | class_attribute :accept_api_auth_actions |
|
27 | 27 | class_attribute :accept_rss_auth_actions |
|
28 | 28 | class_attribute :model_object |
|
29 | 29 | |
|
30 | 30 | layout 'base' |
|
31 | 31 | |
|
32 | 32 | protect_from_forgery |
|
33 | 33 | def handle_unverified_request |
|
34 | 34 | super |
|
35 | 35 | cookies.delete(:autologin) |
|
36 | 36 | end |
|
37 | 37 | |
|
38 | 38 | before_filter :session_expiration, :user_setup, :check_if_login_required, :set_localization |
|
39 | 39 | |
|
40 | 40 | rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token |
|
41 | 41 | rescue_from ::Unauthorized, :with => :deny_access |
|
42 | 42 | rescue_from ::ActionView::MissingTemplate, :with => :missing_template |
|
43 | 43 | |
|
44 | 44 | include Redmine::Search::Controller |
|
45 | 45 | include Redmine::MenuManager::MenuController |
|
46 | 46 | helper Redmine::MenuManager::MenuHelper |
|
47 | 47 | |
|
48 | 48 | def session_expiration |
|
49 | 49 | if session[:user_id] |
|
50 | 50 | if session_expired? && !try_to_autologin |
|
51 | 51 | reset_session |
|
52 | 52 | flash[:error] = l(:error_session_expired) |
|
53 | 53 | redirect_to signin_url |
|
54 | 54 | else |
|
55 | 55 | session[:atime] = Time.now.utc.to_i |
|
56 | 56 | end |
|
57 | 57 | end |
|
58 | 58 | end |
|
59 | 59 | |
|
60 | 60 | def session_expired? |
|
61 | 61 | if Setting.session_lifetime? |
|
62 | 62 | unless session[:ctime] && (Time.now.utc.to_i - session[:ctime].to_i <= Setting.session_lifetime.to_i * 60) |
|
63 | 63 | return true |
|
64 | 64 | end |
|
65 | 65 | end |
|
66 | 66 | if Setting.session_timeout? |
|
67 | 67 | unless session[:atime] && (Time.now.utc.to_i - session[:atime].to_i <= Setting.session_timeout.to_i * 60) |
|
68 | 68 | return true |
|
69 | 69 | end |
|
70 | 70 | end |
|
71 | 71 | false |
|
72 | 72 | end |
|
73 | 73 | |
|
74 | 74 | def start_user_session(user) |
|
75 | 75 | session[:user_id] = user.id |
|
76 | 76 | session[:ctime] = Time.now.utc.to_i |
|
77 | 77 | session[:atime] = Time.now.utc.to_i |
|
78 | 78 | end |
|
79 | 79 | |
|
80 | 80 | def user_setup |
|
81 | 81 | # Check the settings cache for each request |
|
82 | 82 | Setting.check_cache |
|
83 | 83 | # Find the current user |
|
84 | 84 | User.current = find_current_user |
|
85 | 85 | logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger |
|
86 | 86 | end |
|
87 | 87 | |
|
88 | 88 | # Returns the current user or nil if no user is logged in |
|
89 | 89 | # and starts a session if needed |
|
90 | 90 | def find_current_user |
|
91 | 91 | user = nil |
|
92 | 92 | unless api_request? |
|
93 | 93 | if session[:user_id] |
|
94 | 94 | # existing session |
|
95 | 95 | user = (User.active.find(session[:user_id]) rescue nil) |
|
96 | 96 | elsif autologin_user = try_to_autologin |
|
97 | 97 | user = autologin_user |
|
98 | 98 | elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth? |
|
99 | 99 | # RSS key authentication does not start a session |
|
100 | 100 | user = User.find_by_rss_key(params[:key]) |
|
101 | 101 | end |
|
102 | 102 | end |
|
103 | 103 | if user.nil? && Setting.rest_api_enabled? && accept_api_auth? |
|
104 | 104 | if (key = api_key_from_request) |
|
105 | 105 | # Use API key |
|
106 | 106 | user = User.find_by_api_key(key) |
|
107 | 107 | else |
|
108 | 108 | # HTTP Basic, either username/password or API key/random |
|
109 | 109 | authenticate_with_http_basic do |username, password| |
|
110 | 110 | user = User.try_to_login(username, password) || User.find_by_api_key(username) |
|
111 | 111 | end |
|
112 | 112 | end |
|
113 | 113 | # Switch user if requested by an admin user |
|
114 | 114 | if user && user.admin? && (username = api_switch_user_from_request) |
|
115 | 115 | su = User.find_by_login(username) |
|
116 | 116 | if su && su.active? |
|
117 | 117 | logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger |
|
118 | 118 | user = su |
|
119 | 119 | else |
|
120 | 120 | render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412 |
|
121 | 121 | end |
|
122 | 122 | end |
|
123 | 123 | end |
|
124 | 124 | user |
|
125 | 125 | end |
|
126 | 126 | |
|
127 | 127 | def try_to_autologin |
|
128 | 128 | if cookies[:autologin] && Setting.autologin? |
|
129 | 129 | # auto-login feature starts a new session |
|
130 | 130 | user = User.try_to_autologin(cookies[:autologin]) |
|
131 | 131 | if user |
|
132 | 132 | reset_session |
|
133 | 133 | start_user_session(user) |
|
134 | 134 | end |
|
135 | 135 | user |
|
136 | 136 | end |
|
137 | 137 | end |
|
138 | 138 | |
|
139 | 139 | # Sets the logged in user |
|
140 | 140 | def logged_user=(user) |
|
141 | 141 | reset_session |
|
142 | 142 | if user && user.is_a?(User) |
|
143 | 143 | User.current = user |
|
144 | 144 | start_user_session(user) |
|
145 | 145 | else |
|
146 | 146 | User.current = User.anonymous |
|
147 | 147 | end |
|
148 | 148 | end |
|
149 | 149 | |
|
150 | 150 | # Logs out current user |
|
151 | 151 | def logout_user |
|
152 | 152 | if User.current.logged? |
|
153 | 153 | cookies.delete :autologin |
|
154 | 154 | Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin']) |
|
155 | 155 | self.logged_user = nil |
|
156 | 156 | end |
|
157 | 157 | end |
|
158 | 158 | |
|
159 | 159 | # check if login is globally required to access the application |
|
160 | 160 | def check_if_login_required |
|
161 | 161 | # no check needed if user is already logged in |
|
162 | 162 | return true if User.current.logged? |
|
163 | 163 | require_login if Setting.login_required? |
|
164 | 164 | end |
|
165 | 165 | |
|
166 | 166 | def set_localization |
|
167 | 167 | lang = nil |
|
168 | 168 | if User.current.logged? |
|
169 | 169 | lang = find_language(User.current.language) |
|
170 | 170 | end |
|
171 | 171 | if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE'] |
|
172 | 172 | accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first |
|
173 | 173 | if !accept_lang.blank? |
|
174 | 174 | accept_lang = accept_lang.downcase |
|
175 | 175 | lang = find_language(accept_lang) || find_language(accept_lang.split('-').first) |
|
176 | 176 | end |
|
177 | 177 | end |
|
178 | 178 | lang ||= Setting.default_language |
|
179 | 179 | set_language_if_valid(lang) |
|
180 | 180 | end |
|
181 | 181 | |
|
182 | 182 | def require_login |
|
183 | 183 | if !User.current.logged? |
|
184 | 184 | # Extract only the basic url parameters on non-GET requests |
|
185 | 185 | if request.get? |
|
186 | 186 | url = url_for(params) |
|
187 | 187 | else |
|
188 | 188 | url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id]) |
|
189 | 189 | end |
|
190 | 190 | respond_to do |format| |
|
191 | 191 | format.html { redirect_to :controller => "account", :action => "login", :back_url => url } |
|
192 | 192 | format.atom { redirect_to :controller => "account", :action => "login", :back_url => url } |
|
193 | 193 | format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
194 | 194 | format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
195 | 195 | format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' } |
|
196 | 196 | end |
|
197 | 197 | return false |
|
198 | 198 | end |
|
199 | 199 | true |
|
200 | 200 | end |
|
201 | 201 | |
|
202 | 202 | def require_admin |
|
203 | 203 | return unless require_login |
|
204 | 204 | if !User.current.admin? |
|
205 | 205 | render_403 |
|
206 | 206 | return false |
|
207 | 207 | end |
|
208 | 208 | true |
|
209 | 209 | end |
|
210 | 210 | |
|
211 | 211 | def deny_access |
|
212 | 212 | User.current.logged? ? render_403 : require_login |
|
213 | 213 | end |
|
214 | 214 | |
|
215 | 215 | # Authorize the user for the requested action |
|
216 | 216 | def authorize(ctrl = params[:controller], action = params[:action], global = false) |
|
217 | 217 | allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global) |
|
218 | 218 | if allowed |
|
219 | 219 | true |
|
220 | 220 | else |
|
221 | 221 | if @project && @project.archived? |
|
222 | 222 | render_403 :message => :notice_not_authorized_archived_project |
|
223 | 223 | else |
|
224 | 224 | deny_access |
|
225 | 225 | end |
|
226 | 226 | end |
|
227 | 227 | end |
|
228 | 228 | |
|
229 | 229 | # Authorize the user for the requested action outside a project |
|
230 | 230 | def authorize_global(ctrl = params[:controller], action = params[:action], global = true) |
|
231 | 231 | authorize(ctrl, action, global) |
|
232 | 232 | end |
|
233 | 233 | |
|
234 | 234 | # Find project of id params[:id] |
|
235 | 235 | def find_project |
|
236 | 236 | @project = Project.find(params[:id]) |
|
237 | 237 | rescue ActiveRecord::RecordNotFound |
|
238 | 238 | render_404 |
|
239 | 239 | end |
|
240 | 240 | |
|
241 | 241 | # Find project of id params[:project_id] |
|
242 | 242 | def find_project_by_project_id |
|
243 | 243 | @project = Project.find(params[:project_id]) |
|
244 | 244 | rescue ActiveRecord::RecordNotFound |
|
245 | 245 | render_404 |
|
246 | 246 | end |
|
247 | 247 | |
|
248 | 248 | # Find a project based on params[:project_id] |
|
249 | 249 | # TODO: some subclasses override this, see about merging their logic |
|
250 | 250 | def find_optional_project |
|
251 | 251 | @project = Project.find(params[:project_id]) unless params[:project_id].blank? |
|
252 | 252 | allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true) |
|
253 | 253 | allowed ? true : deny_access |
|
254 | 254 | rescue ActiveRecord::RecordNotFound |
|
255 | 255 | render_404 |
|
256 | 256 | end |
|
257 | 257 | |
|
258 | 258 | # Finds and sets @project based on @object.project |
|
259 | 259 | def find_project_from_association |
|
260 | 260 | render_404 unless @object.present? |
|
261 | 261 | |
|
262 | 262 | @project = @object.project |
|
263 | 263 | end |
|
264 | 264 | |
|
265 | 265 | def find_model_object |
|
266 | 266 | model = self.class.model_object |
|
267 | 267 | if model |
|
268 | 268 | @object = model.find(params[:id]) |
|
269 | 269 | self.instance_variable_set('@' + controller_name.singularize, @object) if @object |
|
270 | 270 | end |
|
271 | 271 | rescue ActiveRecord::RecordNotFound |
|
272 | 272 | render_404 |
|
273 | 273 | end |
|
274 | 274 | |
|
275 | 275 | def self.model_object(model) |
|
276 | 276 | self.model_object = model |
|
277 | 277 | end |
|
278 | 278 | |
|
279 | 279 | # Find the issue whose id is the :id parameter |
|
280 | 280 | # Raises a Unauthorized exception if the issue is not visible |
|
281 | 281 | def find_issue |
|
282 | 282 | # Issue.visible.find(...) can not be used to redirect user to the login form |
|
283 | 283 | # if the issue actually exists but requires authentication |
|
284 | 284 | @issue = Issue.find(params[:id]) |
|
285 | 285 | raise Unauthorized unless @issue.visible? |
|
286 | 286 | @project = @issue.project |
|
287 | 287 | rescue ActiveRecord::RecordNotFound |
|
288 | 288 | render_404 |
|
289 | 289 | end |
|
290 | 290 | |
|
291 | 291 | # Find issues with a single :id param or :ids array param |
|
292 | 292 | # Raises a Unauthorized exception if one of the issues is not visible |
|
293 | 293 | def find_issues |
|
294 | 294 | @issues = Issue.find_all_by_id(params[:id] || params[:ids]) |
|
295 | 295 | raise ActiveRecord::RecordNotFound if @issues.empty? |
|
296 | 296 | raise Unauthorized unless @issues.all?(&:visible?) |
|
297 | 297 | @projects = @issues.collect(&:project).compact.uniq |
|
298 | 298 | @project = @projects.first if @projects.size == 1 |
|
299 | 299 | rescue ActiveRecord::RecordNotFound |
|
300 | 300 | render_404 |
|
301 | 301 | end |
|
302 | 302 | |
|
303 | def find_attachments | |
|
304 | if (attachments = params[:attachments]).present? | |
|
305 | att = attachments.values.collect do |attachment| | |
|
306 | Attachment.find_by_token( attachment[:token] ) if attachment[:token].present? | |
|
307 | end | |
|
308 | att.compact! | |
|
309 | end | |
|
310 | @attachments = att || [] | |
|
311 | end | |
|
312 | ||
|
303 | 313 | # make sure that the user is a member of the project (or admin) if project is private |
|
304 | 314 | # used as a before_filter for actions that do not require any particular permission on the project |
|
305 | 315 | def check_project_privacy |
|
306 | 316 | if @project && !@project.archived? |
|
307 | 317 | if @project.visible? |
|
308 | 318 | true |
|
309 | 319 | else |
|
310 | 320 | deny_access |
|
311 | 321 | end |
|
312 | 322 | else |
|
313 | 323 | @project = nil |
|
314 | 324 | render_404 |
|
315 | 325 | false |
|
316 | 326 | end |
|
317 | 327 | end |
|
318 | 328 | |
|
319 | 329 | def back_url |
|
320 | 330 | url = params[:back_url] |
|
321 | 331 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
322 | 332 | url = CGI.unescape(referer.to_s) |
|
323 | 333 | end |
|
324 | 334 | url |
|
325 | 335 | end |
|
326 | 336 | |
|
327 | 337 | def redirect_back_or_default(default) |
|
328 | 338 | back_url = params[:back_url].to_s |
|
329 | 339 | if back_url.present? |
|
330 | 340 | begin |
|
331 | 341 | uri = URI.parse(back_url) |
|
332 | 342 | # do not redirect user to another host or to the login or register page |
|
333 | 343 | if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)}) |
|
334 | 344 | redirect_to(back_url) |
|
335 | 345 | return |
|
336 | 346 | end |
|
337 | 347 | rescue URI::InvalidURIError |
|
338 | 348 | logger.warn("Could not redirect to invalid URL #{back_url}") |
|
339 | 349 | # redirect to default |
|
340 | 350 | end |
|
341 | 351 | end |
|
342 | 352 | redirect_to default |
|
343 | 353 | false |
|
344 | 354 | end |
|
345 | 355 | |
|
346 | 356 | # Redirects to the request referer if present, redirects to args or call block otherwise. |
|
347 | 357 | def redirect_to_referer_or(*args, &block) |
|
348 | 358 | redirect_to :back |
|
349 | 359 | rescue ::ActionController::RedirectBackError |
|
350 | 360 | if args.any? |
|
351 | 361 | redirect_to *args |
|
352 | 362 | elsif block_given? |
|
353 | 363 | block.call |
|
354 | 364 | else |
|
355 | 365 | raise "#redirect_to_referer_or takes arguments or a block" |
|
356 | 366 | end |
|
357 | 367 | end |
|
358 | 368 | |
|
359 | 369 | def render_403(options={}) |
|
360 | 370 | @project = nil |
|
361 | 371 | render_error({:message => :notice_not_authorized, :status => 403}.merge(options)) |
|
362 | 372 | return false |
|
363 | 373 | end |
|
364 | 374 | |
|
365 | 375 | def render_404(options={}) |
|
366 | 376 | render_error({:message => :notice_file_not_found, :status => 404}.merge(options)) |
|
367 | 377 | return false |
|
368 | 378 | end |
|
369 | 379 | |
|
370 | 380 | # Renders an error response |
|
371 | 381 | def render_error(arg) |
|
372 | 382 | arg = {:message => arg} unless arg.is_a?(Hash) |
|
373 | 383 | |
|
374 | 384 | @message = arg[:message] |
|
375 | 385 | @message = l(@message) if @message.is_a?(Symbol) |
|
376 | 386 | @status = arg[:status] || 500 |
|
377 | 387 | |
|
378 | 388 | respond_to do |format| |
|
379 | 389 | format.html { |
|
380 | 390 | render :template => 'common/error', :layout => use_layout, :status => @status |
|
381 | 391 | } |
|
382 | 392 | format.any { head @status } |
|
383 | 393 | end |
|
384 | 394 | end |
|
385 | 395 | |
|
386 | 396 | # Handler for ActionView::MissingTemplate exception |
|
387 | 397 | def missing_template |
|
388 | 398 | logger.warn "Missing template, responding with 404" |
|
389 | 399 | @project = nil |
|
390 | 400 | render_404 |
|
391 | 401 | end |
|
392 | 402 | |
|
393 | 403 | # Filter for actions that provide an API response |
|
394 | 404 | # but have no HTML representation for non admin users |
|
395 | 405 | def require_admin_or_api_request |
|
396 | 406 | return true if api_request? |
|
397 | 407 | if User.current.admin? |
|
398 | 408 | true |
|
399 | 409 | elsif User.current.logged? |
|
400 | 410 | render_error(:status => 406) |
|
401 | 411 | else |
|
402 | 412 | deny_access |
|
403 | 413 | end |
|
404 | 414 | end |
|
405 | 415 | |
|
406 | 416 | # Picks which layout to use based on the request |
|
407 | 417 | # |
|
408 | 418 | # @return [boolean, string] name of the layout to use or false for no layout |
|
409 | 419 | def use_layout |
|
410 | 420 | request.xhr? ? false : 'base' |
|
411 | 421 | end |
|
412 | 422 | |
|
413 | 423 | def invalid_authenticity_token |
|
414 | 424 | if api_request? |
|
415 | 425 | logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)." |
|
416 | 426 | end |
|
417 | 427 | render_error "Invalid form authenticity token." |
|
418 | 428 | end |
|
419 | 429 | |
|
420 | 430 | def render_feed(items, options={}) |
|
421 | 431 | @items = items || [] |
|
422 | 432 | @items.sort! {|x,y| y.event_datetime <=> x.event_datetime } |
|
423 | 433 | @items = @items.slice(0, Setting.feeds_limit.to_i) |
|
424 | 434 | @title = options[:title] || Setting.app_title |
|
425 | 435 | render :template => "common/feed", :formats => [:atom], :layout => false, |
|
426 | 436 | :content_type => 'application/atom+xml' |
|
427 | 437 | end |
|
428 | 438 | |
|
429 | 439 | def self.accept_rss_auth(*actions) |
|
430 | 440 | if actions.any? |
|
431 | 441 | self.accept_rss_auth_actions = actions |
|
432 | 442 | else |
|
433 | 443 | self.accept_rss_auth_actions || [] |
|
434 | 444 | end |
|
435 | 445 | end |
|
436 | 446 | |
|
437 | 447 | def accept_rss_auth?(action=action_name) |
|
438 | 448 | self.class.accept_rss_auth.include?(action.to_sym) |
|
439 | 449 | end |
|
440 | 450 | |
|
441 | 451 | def self.accept_api_auth(*actions) |
|
442 | 452 | if actions.any? |
|
443 | 453 | self.accept_api_auth_actions = actions |
|
444 | 454 | else |
|
445 | 455 | self.accept_api_auth_actions || [] |
|
446 | 456 | end |
|
447 | 457 | end |
|
448 | 458 | |
|
449 | 459 | def accept_api_auth?(action=action_name) |
|
450 | 460 | self.class.accept_api_auth.include?(action.to_sym) |
|
451 | 461 | end |
|
452 | 462 | |
|
453 | 463 | # Returns the number of objects that should be displayed |
|
454 | 464 | # on the paginated list |
|
455 | 465 | def per_page_option |
|
456 | 466 | per_page = nil |
|
457 | 467 | if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i) |
|
458 | 468 | per_page = params[:per_page].to_s.to_i |
|
459 | 469 | session[:per_page] = per_page |
|
460 | 470 | elsif session[:per_page] |
|
461 | 471 | per_page = session[:per_page] |
|
462 | 472 | else |
|
463 | 473 | per_page = Setting.per_page_options_array.first || 25 |
|
464 | 474 | end |
|
465 | 475 | per_page |
|
466 | 476 | end |
|
467 | 477 | |
|
468 | 478 | # Returns offset and limit used to retrieve objects |
|
469 | 479 | # for an API response based on offset, limit and page parameters |
|
470 | 480 | def api_offset_and_limit(options=params) |
|
471 | 481 | if options[:offset].present? |
|
472 | 482 | offset = options[:offset].to_i |
|
473 | 483 | if offset < 0 |
|
474 | 484 | offset = 0 |
|
475 | 485 | end |
|
476 | 486 | end |
|
477 | 487 | limit = options[:limit].to_i |
|
478 | 488 | if limit < 1 |
|
479 | 489 | limit = 25 |
|
480 | 490 | elsif limit > 100 |
|
481 | 491 | limit = 100 |
|
482 | 492 | end |
|
483 | 493 | if offset.nil? && options[:page].present? |
|
484 | 494 | offset = (options[:page].to_i - 1) * limit |
|
485 | 495 | offset = 0 if offset < 0 |
|
486 | 496 | end |
|
487 | 497 | offset ||= 0 |
|
488 | 498 | |
|
489 | 499 | [offset, limit] |
|
490 | 500 | end |
|
491 | 501 | |
|
492 | 502 | # qvalues http header parser |
|
493 | 503 | # code taken from webrick |
|
494 | 504 | def parse_qvalues(value) |
|
495 | 505 | tmp = [] |
|
496 | 506 | if value |
|
497 | 507 | parts = value.split(/,\s*/) |
|
498 | 508 | parts.each {|part| |
|
499 | 509 | if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part) |
|
500 | 510 | val = m[1] |
|
501 | 511 | q = (m[2] or 1).to_f |
|
502 | 512 | tmp.push([val, q]) |
|
503 | 513 | end |
|
504 | 514 | } |
|
505 | 515 | tmp = tmp.sort_by{|val, q| -q} |
|
506 | 516 | tmp.collect!{|val, q| val} |
|
507 | 517 | end |
|
508 | 518 | return tmp |
|
509 | 519 | rescue |
|
510 | 520 | nil |
|
511 | 521 | end |
|
512 | 522 | |
|
513 | 523 | # Returns a string that can be used as filename value in Content-Disposition header |
|
514 | 524 | def filename_for_content_disposition(name) |
|
515 | 525 | request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name |
|
516 | 526 | end |
|
517 | 527 | |
|
518 | 528 | def api_request? |
|
519 | 529 | %w(xml json).include? params[:format] |
|
520 | 530 | end |
|
521 | 531 | |
|
522 | 532 | # Returns the API key present in the request |
|
523 | 533 | def api_key_from_request |
|
524 | 534 | if params[:key].present? |
|
525 | 535 | params[:key].to_s |
|
526 | 536 | elsif request.headers["X-Redmine-API-Key"].present? |
|
527 | 537 | request.headers["X-Redmine-API-Key"].to_s |
|
528 | 538 | end |
|
529 | 539 | end |
|
530 | 540 | |
|
531 | 541 | # Returns the API 'switch user' value if present |
|
532 | 542 | def api_switch_user_from_request |
|
533 | 543 | request.headers["X-Redmine-Switch-User"].to_s.presence |
|
534 | 544 | end |
|
535 | 545 | |
|
536 | 546 | # Renders a warning flash if obj has unsaved attachments |
|
537 | 547 | def render_attachment_warning_if_needed(obj) |
|
538 | 548 | flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present? |
|
539 | 549 | end |
|
540 | 550 | |
|
541 | 551 | # Sets the `flash` notice or error based the number of issues that did not save |
|
542 | 552 | # |
|
543 | 553 | # @param [Array, Issue] issues all of the saved and unsaved Issues |
|
544 | 554 | # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved |
|
545 | 555 | def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids) |
|
546 | 556 | if unsaved_issue_ids.empty? |
|
547 | 557 | flash[:notice] = l(:notice_successful_update) unless issues.empty? |
|
548 | 558 | else |
|
549 | 559 | flash[:error] = l(:notice_failed_to_save_issues, |
|
550 | 560 | :count => unsaved_issue_ids.size, |
|
551 | 561 | :total => issues.size, |
|
552 | 562 | :ids => '#' + unsaved_issue_ids.join(', #')) |
|
553 | 563 | end |
|
554 | 564 | end |
|
555 | 565 | |
|
556 | 566 | # Rescues an invalid query statement. Just in case... |
|
557 | 567 | def query_statement_invalid(exception) |
|
558 | 568 | logger.error "Query::StatementInvalid: #{exception.message}" if logger |
|
559 | 569 | session.delete(:query) |
|
560 | 570 | sort_clear if respond_to?(:sort_clear) |
|
561 | 571 | render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator." |
|
562 | 572 | end |
|
563 | 573 | |
|
564 | 574 | # Renders a 200 response for successfull updates or deletions via the API |
|
565 | 575 | def render_api_ok |
|
566 | 576 | render_api_head :ok |
|
567 | 577 | end |
|
568 | 578 | |
|
569 | 579 | # Renders a head API response |
|
570 | 580 | def render_api_head(status) |
|
571 | 581 | # #head would return a response body with one space |
|
572 | 582 | render :text => '', :status => status, :layout => nil |
|
573 | 583 | end |
|
574 | 584 | |
|
575 | 585 | # Renders API response on validation failure |
|
576 | 586 | def render_validation_errors(objects) |
|
577 | 587 | if objects.is_a?(Array) |
|
578 | 588 | @error_messages = objects.map {|object| object.errors.full_messages}.flatten |
|
579 | 589 | else |
|
580 | 590 | @error_messages = objects.errors.full_messages |
|
581 | 591 | end |
|
582 | 592 | render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil |
|
583 | 593 | end |
|
584 | 594 | |
|
585 | 595 | # Overrides #_include_layout? so that #render with no arguments |
|
586 | 596 | # doesn't use the layout for api requests |
|
587 | 597 | def _include_layout?(*args) |
|
588 | 598 | api_request? ? false : super |
|
589 | 599 | end |
|
590 | 600 | end |
@@ -1,139 +1,149 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class AttachmentsController < ApplicationController |
|
19 | 19 | before_filter :find_project, :except => :upload |
|
20 | 20 | before_filter :file_readable, :read_authorize, :only => [:show, :download, :thumbnail] |
|
21 | 21 | before_filter :delete_authorize, :only => :destroy |
|
22 | 22 | before_filter :authorize_global, :only => :upload |
|
23 | 23 | |
|
24 | 24 | accept_api_auth :show, :download, :upload |
|
25 | 25 | |
|
26 | 26 | def show |
|
27 | 27 | respond_to do |format| |
|
28 | 28 | format.html { |
|
29 | 29 | if @attachment.is_diff? |
|
30 | 30 | @diff = File.new(@attachment.diskfile, "rb").read |
|
31 | 31 | @diff_type = params[:type] || User.current.pref[:diff_type] || 'inline' |
|
32 | 32 | @diff_type = 'inline' unless %w(inline sbs).include?(@diff_type) |
|
33 | 33 | # Save diff type as user preference |
|
34 | 34 | if User.current.logged? && @diff_type != User.current.pref[:diff_type] |
|
35 | 35 | User.current.pref[:diff_type] = @diff_type |
|
36 | 36 | User.current.preference.save |
|
37 | 37 | end |
|
38 | 38 | render :action => 'diff' |
|
39 | 39 | elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte |
|
40 | 40 | @content = File.new(@attachment.diskfile, "rb").read |
|
41 | 41 | render :action => 'file' |
|
42 | 42 | else |
|
43 | 43 | download |
|
44 | 44 | end |
|
45 | 45 | } |
|
46 | 46 | format.api |
|
47 | 47 | end |
|
48 | 48 | end |
|
49 | 49 | |
|
50 | 50 | def download |
|
51 | 51 | if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project) |
|
52 | 52 | @attachment.increment_download |
|
53 | 53 | end |
|
54 | 54 | |
|
55 | 55 | if stale?(:etag => @attachment.digest) |
|
56 | 56 | # images are sent inline |
|
57 | 57 | send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename), |
|
58 | 58 | :type => detect_content_type(@attachment), |
|
59 | 59 | :disposition => (@attachment.image? ? 'inline' : 'attachment') |
|
60 | 60 | end |
|
61 | 61 | end |
|
62 | 62 | |
|
63 | 63 | def thumbnail |
|
64 | 64 | if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size]) |
|
65 | 65 | if stale?(:etag => thumbnail) |
|
66 | 66 | send_file thumbnail, |
|
67 | 67 | :filename => filename_for_content_disposition(@attachment.filename), |
|
68 | 68 | :type => detect_content_type(@attachment), |
|
69 | 69 | :disposition => 'inline' |
|
70 | 70 | end |
|
71 | 71 | else |
|
72 | 72 | # No thumbnail for the attachment or thumbnail could not be created |
|
73 | 73 | render :nothing => true, :status => 404 |
|
74 | 74 | end |
|
75 | 75 | end |
|
76 | 76 | |
|
77 | 77 | def upload |
|
78 | 78 | # Make sure that API users get used to set this content type |
|
79 | 79 | # as it won't trigger Rails' automatic parsing of the request body for parameters |
|
80 | 80 | unless request.content_type == 'application/octet-stream' |
|
81 | 81 | render :nothing => true, :status => 406 |
|
82 | 82 | return |
|
83 | 83 | end |
|
84 | 84 | |
|
85 | 85 | @attachment = Attachment.new(:file => request.raw_post) |
|
86 | 86 | @attachment.author = User.current |
|
87 | 87 | @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16) |
|
88 | saved = @attachment.save | |
|
88 | 89 | |
|
89 | if @attachment.save | |
|
90 | respond_to do |format| | |
|
91 | format.api { render :action => 'upload', :status => :created } | |
|
92 |
e |
|
|
93 | else | |
|
94 | respond_to do |format| | |
|
95 |
|
|
|
96 | end | |
|
90 | respond_to do |format| | |
|
91 | format.js | |
|
92 | format.api { | |
|
93 | if saved | |
|
94 | render :action => 'upload', :status => :created | |
|
95 | else | |
|
96 | render_validation_errors(@attachment) | |
|
97 | end | |
|
98 | } | |
|
97 | 99 | end |
|
98 | 100 | end |
|
99 | 101 | |
|
100 | 102 | def destroy |
|
101 | 103 | if @attachment.container.respond_to?(:init_journal) |
|
102 | 104 | @attachment.container.init_journal(User.current) |
|
103 | 105 | end |
|
104 | # Make sure association callbacks are called | |
|
105 | @attachment.container.attachments.delete(@attachment) | |
|
106 | redirect_to_referer_or project_path(@project) | |
|
106 | if @attachment.container | |
|
107 | # Make sure association callbacks are called | |
|
108 | @attachment.container.attachments.delete(@attachment) | |
|
109 | else | |
|
110 | @attachment.destroy | |
|
111 | end | |
|
112 | ||
|
113 | respond_to do |format| | |
|
114 | format.html { redirect_to_referer_or project_path(@project) } | |
|
115 | format.js | |
|
116 | end | |
|
107 | 117 | end |
|
108 | 118 | |
|
109 | 119 | private |
|
110 | 120 | def find_project |
|
111 | 121 | @attachment = Attachment.find(params[:id]) |
|
112 | 122 | # Show 404 if the filename in the url is wrong |
|
113 | 123 | raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename |
|
114 | 124 | @project = @attachment.project |
|
115 | 125 | rescue ActiveRecord::RecordNotFound |
|
116 | 126 | render_404 |
|
117 | 127 | end |
|
118 | 128 | |
|
119 | 129 | # Checks that the file exists and is readable |
|
120 | 130 | def file_readable |
|
121 | 131 | @attachment.readable? ? true : render_404 |
|
122 | 132 | end |
|
123 | 133 | |
|
124 | 134 | def read_authorize |
|
125 | 135 | @attachment.visible? ? true : deny_access |
|
126 | 136 | end |
|
127 | 137 | |
|
128 | 138 | def delete_authorize |
|
129 | 139 | @attachment.deletable? ? true : deny_access |
|
130 | 140 | end |
|
131 | 141 | |
|
132 | 142 | def detect_content_type(attachment) |
|
133 | 143 | content_type = attachment.content_type |
|
134 | 144 | if content_type.blank? |
|
135 | 145 | content_type = Redmine::MimeType.of(attachment.filename) |
|
136 | 146 | end |
|
137 | 147 | content_type.to_s |
|
138 | 148 | end |
|
139 | 149 | end |
@@ -1,141 +1,141 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class MessagesController < ApplicationController |
|
19 | 19 | menu_item :boards |
|
20 | 20 | default_search_scope :messages |
|
21 | 21 | before_filter :find_board, :only => [:new, :preview] |
|
22 | before_filter :find_attachments, :only => [:preview] | |
|
22 | 23 | before_filter :find_message, :except => [:new, :preview] |
|
23 | 24 | before_filter :authorize, :except => [:preview, :edit, :destroy] |
|
24 | 25 | |
|
25 | 26 | helper :boards |
|
26 | 27 | helper :watchers |
|
27 | 28 | helper :attachments |
|
28 | 29 | include AttachmentsHelper |
|
29 | 30 | |
|
30 | 31 | REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE) |
|
31 | 32 | |
|
32 | 33 | # Show a topic and its replies |
|
33 | 34 | def show |
|
34 | 35 | page = params[:page] |
|
35 | 36 | # Find the page of the requested reply |
|
36 | 37 | if params[:r] && page.nil? |
|
37 | 38 | offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i]) |
|
38 | 39 | page = 1 + offset / REPLIES_PER_PAGE |
|
39 | 40 | end |
|
40 | 41 | |
|
41 | 42 | @reply_count = @topic.children.count |
|
42 | 43 | @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page |
|
43 | 44 | @replies = @topic.children. |
|
44 | 45 | includes(:author, :attachments, {:board => :project}). |
|
45 | 46 | reorder("#{Message.table_name}.created_on ASC"). |
|
46 | 47 | limit(@reply_pages.items_per_page). |
|
47 | 48 | offset(@reply_pages.current.offset). |
|
48 | 49 | all |
|
49 | 50 | |
|
50 | 51 | @reply = Message.new(:subject => "RE: #{@message.subject}") |
|
51 | 52 | render :action => "show", :layout => false if request.xhr? |
|
52 | 53 | end |
|
53 | 54 | |
|
54 | 55 | # Create a new topic |
|
55 | 56 | def new |
|
56 | 57 | @message = Message.new |
|
57 | 58 | @message.author = User.current |
|
58 | 59 | @message.board = @board |
|
59 | 60 | @message.safe_attributes = params[:message] |
|
60 | 61 | if request.post? |
|
61 | 62 | @message.save_attachments(params[:attachments]) |
|
62 | 63 | if @message.save |
|
63 | 64 | call_hook(:controller_messages_new_after_save, { :params => params, :message => @message}) |
|
64 | 65 | render_attachment_warning_if_needed(@message) |
|
65 | 66 | redirect_to board_message_path(@board, @message) |
|
66 | 67 | end |
|
67 | 68 | end |
|
68 | 69 | end |
|
69 | 70 | |
|
70 | 71 | # Reply to a topic |
|
71 | 72 | def reply |
|
72 | 73 | @reply = Message.new |
|
73 | 74 | @reply.author = User.current |
|
74 | 75 | @reply.board = @board |
|
75 | 76 | @reply.safe_attributes = params[:reply] |
|
76 | 77 | @topic.children << @reply |
|
77 | 78 | if !@reply.new_record? |
|
78 | 79 | call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply}) |
|
79 | 80 | attachments = Attachment.attach_files(@reply, params[:attachments]) |
|
80 | 81 | render_attachment_warning_if_needed(@reply) |
|
81 | 82 | end |
|
82 | 83 | redirect_to board_message_path(@board, @topic, :r => @reply) |
|
83 | 84 | end |
|
84 | 85 | |
|
85 | 86 | # Edit a message |
|
86 | 87 | def edit |
|
87 | 88 | (render_403; return false) unless @message.editable_by?(User.current) |
|
88 | 89 | @message.safe_attributes = params[:message] |
|
89 | 90 | if request.post? && @message.save |
|
90 | 91 | attachments = Attachment.attach_files(@message, params[:attachments]) |
|
91 | 92 | render_attachment_warning_if_needed(@message) |
|
92 | 93 | flash[:notice] = l(:notice_successful_update) |
|
93 | 94 | @message.reload |
|
94 | 95 | redirect_to board_message_path(@message.board, @message.root, :r => (@message.parent_id && @message.id)) |
|
95 | 96 | end |
|
96 | 97 | end |
|
97 | 98 | |
|
98 | 99 | # Delete a messages |
|
99 | 100 | def destroy |
|
100 | 101 | (render_403; return false) unless @message.destroyable_by?(User.current) |
|
101 | 102 | r = @message.to_param |
|
102 | 103 | @message.destroy |
|
103 | 104 | if @message.parent |
|
104 | 105 | redirect_to board_message_path(@board, @message.parent, :r => r) |
|
105 | 106 | else |
|
106 | 107 | redirect_to project_board_path(@project, @board) |
|
107 | 108 | end |
|
108 | 109 | end |
|
109 | 110 | |
|
110 | 111 | def quote |
|
111 | 112 | @subject = @message.subject |
|
112 | 113 | @subject = "RE: #{@subject}" unless @subject.starts_with?('RE:') |
|
113 | 114 | |
|
114 | 115 | @content = "#{ll(Setting.default_language, :text_user_wrote, @message.author)}\n> " |
|
115 | 116 | @content << @message.content.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n" |
|
116 | 117 | end |
|
117 | 118 | |
|
118 | 119 | def preview |
|
119 | 120 | message = @board.messages.find_by_id(params[:id]) |
|
120 | @attachements = message.attachments if message | |
|
121 | 121 | @text = (params[:message] || params[:reply])[:content] |
|
122 | 122 | @previewed = message |
|
123 | 123 | render :partial => 'common/preview' |
|
124 | 124 | end |
|
125 | 125 | |
|
126 | 126 | private |
|
127 | 127 | def find_message |
|
128 | 128 | find_board |
|
129 | 129 | @message = @board.messages.find(params[:id], :include => :parent) |
|
130 | 130 | @topic = @message.root |
|
131 | 131 | rescue ActiveRecord::RecordNotFound |
|
132 | 132 | render_404 |
|
133 | 133 | end |
|
134 | 134 | |
|
135 | 135 | def find_board |
|
136 | 136 | @board = Board.find(params[:board_id], :include => :project) |
|
137 | 137 | @project = @board.project |
|
138 | 138 | rescue ActiveRecord::RecordNotFound |
|
139 | 139 | render_404 |
|
140 | 140 | end |
|
141 | 141 | end |
@@ -1,55 +1,53 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class PreviewsController < ApplicationController |
|
19 | before_filter :find_project | |
|
19 | before_filter :find_project, :find_attachments | |
|
20 | 20 | |
|
21 | 21 | def issue |
|
22 | 22 | @issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank? |
|
23 | 23 | if @issue |
|
24 | @attachements = @issue.attachments | |
|
25 | 24 | @description = params[:issue] && params[:issue][:description] |
|
26 | 25 | if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n") |
|
27 | 26 | @description = nil |
|
28 | 27 | end |
|
29 | 28 | # params[:notes] is useful for preview of notes in issue history |
|
30 | 29 | @notes = params[:notes] || (params[:issue] ? params[:issue][:notes] : nil) |
|
31 | 30 | else |
|
32 | 31 | @description = (params[:issue] ? params[:issue][:description] : nil) |
|
33 | 32 | end |
|
34 | 33 | render :layout => false |
|
35 | 34 | end |
|
36 | 35 | |
|
37 | 36 | def news |
|
38 | 37 | if params[:id].present? && news = News.visible.find_by_id(params[:id]) |
|
39 | 38 | @previewed = news |
|
40 | @attachments = news.attachments | |
|
41 | 39 | end |
|
42 | 40 | @text = (params[:news] ? params[:news][:description] : nil) |
|
43 | 41 | render :partial => 'common/preview' |
|
44 | 42 | end |
|
45 | 43 | |
|
46 | 44 | private |
|
47 | 45 | |
|
48 | 46 | def find_project |
|
49 | 47 | project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id] |
|
50 | 48 | @project = Project.find(project_id) |
|
51 | 49 | rescue ActiveRecord::RecordNotFound |
|
52 | 50 | render_404 |
|
53 | 51 | end |
|
54 | 52 | |
|
55 | 53 | end |
@@ -1,355 +1,356 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require 'diff' |
|
19 | 19 | |
|
20 | 20 | # The WikiController follows the Rails REST controller pattern but with |
|
21 | 21 | # a few differences |
|
22 | 22 | # |
|
23 | 23 | # * index - shows a list of WikiPages grouped by page or date |
|
24 | 24 | # * new - not used |
|
25 | 25 | # * create - not used |
|
26 | 26 | # * show - will also show the form for creating a new wiki page |
|
27 | 27 | # * edit - used to edit an existing or new page |
|
28 | 28 | # * update - used to save a wiki page update to the database, including new pages |
|
29 | 29 | # * destroy - normal |
|
30 | 30 | # |
|
31 | 31 | # Other member and collection methods are also used |
|
32 | 32 | # |
|
33 | 33 | # TODO: still being worked on |
|
34 | 34 | class WikiController < ApplicationController |
|
35 | 35 | default_search_scope :wiki_pages |
|
36 | 36 | before_filter :find_wiki, :authorize |
|
37 | 37 | before_filter :find_existing_or_new_page, :only => [:show, :edit, :update] |
|
38 | 38 | before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version] |
|
39 | 39 | accept_api_auth :index, :show, :update, :destroy |
|
40 | before_filter :find_attachments, :only => [:preview] | |
|
40 | 41 | |
|
41 | 42 | helper :attachments |
|
42 | 43 | include AttachmentsHelper |
|
43 | 44 | helper :watchers |
|
44 | 45 | include Redmine::Export::PDF |
|
45 | 46 | |
|
46 | 47 | # List of pages, sorted alphabetically and by parent (hierarchy) |
|
47 | 48 | def index |
|
48 | 49 | load_pages_for_index |
|
49 | 50 | |
|
50 | 51 | respond_to do |format| |
|
51 | 52 | format.html { |
|
52 | 53 | @pages_by_parent_id = @pages.group_by(&:parent_id) |
|
53 | 54 | } |
|
54 | 55 | format.api |
|
55 | 56 | end |
|
56 | 57 | end |
|
57 | 58 | |
|
58 | 59 | # List of page, by last update |
|
59 | 60 | def date_index |
|
60 | 61 | load_pages_for_index |
|
61 | 62 | @pages_by_date = @pages.group_by {|p| p.updated_on.to_date} |
|
62 | 63 | end |
|
63 | 64 | |
|
64 | 65 | # display a page (in editing mode if it doesn't exist) |
|
65 | 66 | def show |
|
66 | 67 | if @page.new_record? |
|
67 | 68 | if User.current.allowed_to?(:edit_wiki_pages, @project) && editable? && !api_request? |
|
68 | 69 | edit |
|
69 | 70 | render :action => 'edit' |
|
70 | 71 | else |
|
71 | 72 | render_404 |
|
72 | 73 | end |
|
73 | 74 | return |
|
74 | 75 | end |
|
75 | 76 | if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project) |
|
76 | 77 | deny_access |
|
77 | 78 | return |
|
78 | 79 | end |
|
79 | 80 | @content = @page.content_for_version(params[:version]) |
|
80 | 81 | if User.current.allowed_to?(:export_wiki_pages, @project) |
|
81 | 82 | if params[:format] == 'pdf' |
|
82 | 83 | send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf") |
|
83 | 84 | return |
|
84 | 85 | elsif params[:format] == 'html' |
|
85 | 86 | export = render_to_string :action => 'export', :layout => false |
|
86 | 87 | send_data(export, :type => 'text/html', :filename => "#{@page.title}.html") |
|
87 | 88 | return |
|
88 | 89 | elsif params[:format] == 'txt' |
|
89 | 90 | send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt") |
|
90 | 91 | return |
|
91 | 92 | end |
|
92 | 93 | end |
|
93 | 94 | @editable = editable? |
|
94 | 95 | @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) && |
|
95 | 96 | @content.current_version? && |
|
96 | 97 | Redmine::WikiFormatting.supports_section_edit? |
|
97 | 98 | |
|
98 | 99 | respond_to do |format| |
|
99 | 100 | format.html |
|
100 | 101 | format.api |
|
101 | 102 | end |
|
102 | 103 | end |
|
103 | 104 | |
|
104 | 105 | # edit an existing page or a new one |
|
105 | 106 | def edit |
|
106 | 107 | return render_403 unless editable? |
|
107 | 108 | if @page.new_record? |
|
108 | 109 | @page.content = WikiContent.new(:page => @page) |
|
109 | 110 | if params[:parent].present? |
|
110 | 111 | @page.parent = @page.wiki.find_page(params[:parent].to_s) |
|
111 | 112 | end |
|
112 | 113 | end |
|
113 | 114 | |
|
114 | 115 | @content = @page.content_for_version(params[:version]) |
|
115 | 116 | @content.text = initial_page_content(@page) if @content.text.blank? |
|
116 | 117 | # don't keep previous comment |
|
117 | 118 | @content.comments = nil |
|
118 | 119 | |
|
119 | 120 | # To prevent StaleObjectError exception when reverting to a previous version |
|
120 | 121 | @content.version = @page.content.version |
|
121 | 122 | |
|
122 | 123 | @text = @content.text |
|
123 | 124 | if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? |
|
124 | 125 | @section = params[:section].to_i |
|
125 | 126 | @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section) |
|
126 | 127 | render_404 if @text.blank? |
|
127 | 128 | end |
|
128 | 129 | end |
|
129 | 130 | |
|
130 | 131 | # Creates a new page or updates an existing one |
|
131 | 132 | def update |
|
132 | 133 | return render_403 unless editable? |
|
133 | 134 | was_new_page = @page.new_record? |
|
134 | 135 | @page.content = WikiContent.new(:page => @page) if @page.new_record? |
|
135 | 136 | @page.safe_attributes = params[:wiki_page] |
|
136 | 137 | |
|
137 | 138 | @content = @page.content |
|
138 | 139 | content_params = params[:content] |
|
139 | 140 | if content_params.nil? && params[:wiki_page].is_a?(Hash) |
|
140 | 141 | content_params = params[:wiki_page].slice(:text, :comments, :version) |
|
141 | 142 | end |
|
142 | 143 | content_params ||= {} |
|
143 | 144 | |
|
144 | 145 | @content.comments = content_params[:comments] |
|
145 | 146 | @text = content_params[:text] |
|
146 | 147 | if params[:section].present? && Redmine::WikiFormatting.supports_section_edit? |
|
147 | 148 | @section = params[:section].to_i |
|
148 | 149 | @section_hash = params[:section_hash] |
|
149 | 150 | @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash) |
|
150 | 151 | else |
|
151 | 152 | @content.version = content_params[:version] if content_params[:version] |
|
152 | 153 | @content.text = @text |
|
153 | 154 | end |
|
154 | 155 | @content.author = User.current |
|
155 | 156 | |
|
156 | 157 | if @page.save_with_content |
|
157 | 158 | attachments = Attachment.attach_files(@page, params[:attachments]) |
|
158 | 159 | render_attachment_warning_if_needed(@page) |
|
159 | 160 | call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page}) |
|
160 | 161 | |
|
161 | 162 | respond_to do |format| |
|
162 | 163 | format.html { redirect_to :action => 'show', :project_id => @project, :id => @page.title } |
|
163 | 164 | format.api { |
|
164 | 165 | if was_new_page |
|
165 | 166 | render :action => 'show', :status => :created, :location => url_for(:controller => 'wiki', :action => 'show', :project_id => @project, :id => @page.title) |
|
166 | 167 | else |
|
167 | 168 | render_api_ok |
|
168 | 169 | end |
|
169 | 170 | } |
|
170 | 171 | end |
|
171 | 172 | else |
|
172 | 173 | respond_to do |format| |
|
173 | 174 | format.html { render :action => 'edit' } |
|
174 | 175 | format.api { render_validation_errors(@content) } |
|
175 | 176 | end |
|
176 | 177 | end |
|
177 | 178 | |
|
178 | 179 | rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError |
|
179 | 180 | # Optimistic locking exception |
|
180 | 181 | respond_to do |format| |
|
181 | 182 | format.html { |
|
182 | 183 | flash.now[:error] = l(:notice_locking_conflict) |
|
183 | 184 | render :action => 'edit' |
|
184 | 185 | } |
|
185 | 186 | format.api { render_api_head :conflict } |
|
186 | 187 | end |
|
187 | 188 | rescue ActiveRecord::RecordNotSaved |
|
188 | 189 | respond_to do |format| |
|
189 | 190 | format.html { render :action => 'edit' } |
|
190 | 191 | format.api { render_validation_errors(@content) } |
|
191 | 192 | end |
|
192 | 193 | end |
|
193 | 194 | |
|
194 | 195 | # rename a page |
|
195 | 196 | def rename |
|
196 | 197 | return render_403 unless editable? |
|
197 | 198 | @page.redirect_existing_links = true |
|
198 | 199 | # used to display the *original* title if some AR validation errors occur |
|
199 | 200 | @original_title = @page.pretty_title |
|
200 | 201 | if request.post? && @page.update_attributes(params[:wiki_page]) |
|
201 | 202 | flash[:notice] = l(:notice_successful_update) |
|
202 | 203 | redirect_to :action => 'show', :project_id => @project, :id => @page.title |
|
203 | 204 | end |
|
204 | 205 | end |
|
205 | 206 | |
|
206 | 207 | def protect |
|
207 | 208 | @page.update_attribute :protected, params[:protected] |
|
208 | 209 | redirect_to :action => 'show', :project_id => @project, :id => @page.title |
|
209 | 210 | end |
|
210 | 211 | |
|
211 | 212 | # show page history |
|
212 | 213 | def history |
|
213 | 214 | @version_count = @page.content.versions.count |
|
214 | 215 | @version_pages = Paginator.new self, @version_count, per_page_option, params['page'] |
|
215 | 216 | # don't load text |
|
216 | 217 | @versions = @page.content.versions. |
|
217 | 218 | select("id, author_id, comments, updated_on, version"). |
|
218 | 219 | reorder('version DESC'). |
|
219 | 220 | limit(@version_pages.items_per_page + 1). |
|
220 | 221 | offset(@version_pages.current.offset). |
|
221 | 222 | all |
|
222 | 223 | |
|
223 | 224 | render :layout => false if request.xhr? |
|
224 | 225 | end |
|
225 | 226 | |
|
226 | 227 | def diff |
|
227 | 228 | @diff = @page.diff(params[:version], params[:version_from]) |
|
228 | 229 | render_404 unless @diff |
|
229 | 230 | end |
|
230 | 231 | |
|
231 | 232 | def annotate |
|
232 | 233 | @annotate = @page.annotate(params[:version]) |
|
233 | 234 | render_404 unless @annotate |
|
234 | 235 | end |
|
235 | 236 | |
|
236 | 237 | # Removes a wiki page and its history |
|
237 | 238 | # Children can be either set as root pages, removed or reassigned to another parent page |
|
238 | 239 | def destroy |
|
239 | 240 | return render_403 unless editable? |
|
240 | 241 | |
|
241 | 242 | @descendants_count = @page.descendants.size |
|
242 | 243 | if @descendants_count > 0 |
|
243 | 244 | case params[:todo] |
|
244 | 245 | when 'nullify' |
|
245 | 246 | # Nothing to do |
|
246 | 247 | when 'destroy' |
|
247 | 248 | # Removes all its descendants |
|
248 | 249 | @page.descendants.each(&:destroy) |
|
249 | 250 | when 'reassign' |
|
250 | 251 | # Reassign children to another parent page |
|
251 | 252 | reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i) |
|
252 | 253 | return unless reassign_to |
|
253 | 254 | @page.children.each do |child| |
|
254 | 255 | child.update_attribute(:parent, reassign_to) |
|
255 | 256 | end |
|
256 | 257 | else |
|
257 | 258 | @reassignable_to = @wiki.pages - @page.self_and_descendants |
|
258 | 259 | # display the destroy form if it's a user request |
|
259 | 260 | return unless api_request? |
|
260 | 261 | end |
|
261 | 262 | end |
|
262 | 263 | @page.destroy |
|
263 | 264 | respond_to do |format| |
|
264 | 265 | format.html { redirect_to :action => 'index', :project_id => @project } |
|
265 | 266 | format.api { render_api_ok } |
|
266 | 267 | end |
|
267 | 268 | end |
|
268 | 269 | |
|
269 | 270 | def destroy_version |
|
270 | 271 | return render_403 unless editable? |
|
271 | 272 | |
|
272 | 273 | @content = @page.content_for_version(params[:version]) |
|
273 | 274 | @content.destroy |
|
274 | 275 | redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project |
|
275 | 276 | end |
|
276 | 277 | |
|
277 | 278 | # Export wiki to a single pdf or html file |
|
278 | 279 | def export |
|
279 | 280 | @pages = @wiki.pages.all(:order => 'title', :include => [:content, {:attachments => :author}]) |
|
280 | 281 | respond_to do |format| |
|
281 | 282 | format.html { |
|
282 | 283 | export = render_to_string :action => 'export_multiple', :layout => false |
|
283 | 284 | send_data(export, :type => 'text/html', :filename => "wiki.html") |
|
284 | 285 | } |
|
285 | 286 | format.pdf { |
|
286 | 287 | send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf") |
|
287 | 288 | } |
|
288 | 289 | end |
|
289 | 290 | end |
|
290 | 291 | |
|
291 | 292 | def preview |
|
292 | 293 | page = @wiki.find_page(params[:id]) |
|
293 | 294 | # page is nil when previewing a new page |
|
294 | 295 | return render_403 unless page.nil? || editable?(page) |
|
295 | 296 | if page |
|
296 |
@attach |
|
|
297 | @attachments += page.attachments | |
|
297 | 298 | @previewed = page.content |
|
298 | 299 | end |
|
299 | 300 | @text = params[:content][:text] |
|
300 | 301 | render :partial => 'common/preview' |
|
301 | 302 | end |
|
302 | 303 | |
|
303 | 304 | def add_attachment |
|
304 | 305 | return render_403 unless editable? |
|
305 | 306 | attachments = Attachment.attach_files(@page, params[:attachments]) |
|
306 | 307 | render_attachment_warning_if_needed(@page) |
|
307 | 308 | redirect_to :action => 'show', :id => @page.title, :project_id => @project |
|
308 | 309 | end |
|
309 | 310 | |
|
310 | 311 | private |
|
311 | 312 | |
|
312 | 313 | def find_wiki |
|
313 | 314 | @project = Project.find(params[:project_id]) |
|
314 | 315 | @wiki = @project.wiki |
|
315 | 316 | render_404 unless @wiki |
|
316 | 317 | rescue ActiveRecord::RecordNotFound |
|
317 | 318 | render_404 |
|
318 | 319 | end |
|
319 | 320 | |
|
320 | 321 | # Finds the requested page or a new page if it doesn't exist |
|
321 | 322 | def find_existing_or_new_page |
|
322 | 323 | @page = @wiki.find_or_new_page(params[:id]) |
|
323 | 324 | if @wiki.page_found_with_redirect? |
|
324 | 325 | redirect_to params.update(:id => @page.title) |
|
325 | 326 | end |
|
326 | 327 | end |
|
327 | 328 | |
|
328 | 329 | # Finds the requested page and returns a 404 error if it doesn't exist |
|
329 | 330 | def find_existing_page |
|
330 | 331 | @page = @wiki.find_page(params[:id]) |
|
331 | 332 | if @page.nil? |
|
332 | 333 | render_404 |
|
333 | 334 | return |
|
334 | 335 | end |
|
335 | 336 | if @wiki.page_found_with_redirect? |
|
336 | 337 | redirect_to params.update(:id => @page.title) |
|
337 | 338 | end |
|
338 | 339 | end |
|
339 | 340 | |
|
340 | 341 | # Returns true if the current user is allowed to edit the page, otherwise false |
|
341 | 342 | def editable?(page = @page) |
|
342 | 343 | page.editable_by?(User.current) |
|
343 | 344 | end |
|
344 | 345 | |
|
345 | 346 | # Returns the default content of a new wiki page |
|
346 | 347 | def initial_page_content(page) |
|
347 | 348 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
348 | 349 | extend helper unless self.instance_of?(helper) |
|
349 | 350 | helper.instance_method(:initial_page_content).bind(self).call(page) |
|
350 | 351 | end |
|
351 | 352 | |
|
352 | 353 | def load_pages_for_index |
|
353 | 354 | @pages = @wiki.pages.with_updated_on.order("#{WikiPage.table_name}.title").includes(:wiki => :project).includes(:parent).all |
|
354 | 355 | end |
|
355 | 356 | end |
@@ -1,1285 +1,1286 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require 'forwardable' |
|
21 | 21 | require 'cgi' |
|
22 | 22 | |
|
23 | 23 | module ApplicationHelper |
|
24 | 24 | include Redmine::WikiFormatting::Macros::Definitions |
|
25 | 25 | include Redmine::I18n |
|
26 | 26 | include GravatarHelper::PublicMethods |
|
27 | 27 | |
|
28 | 28 | extend Forwardable |
|
29 | 29 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
30 | 30 | |
|
31 | 31 | # Return true if user is authorized for controller/action, otherwise false |
|
32 | 32 | def authorize_for(controller, action) |
|
33 | 33 | User.current.allowed_to?({:controller => controller, :action => action}, @project) |
|
34 | 34 | end |
|
35 | 35 | |
|
36 | 36 | # Display a link if user is authorized |
|
37 | 37 | # |
|
38 | 38 | # @param [String] name Anchor text (passed to link_to) |
|
39 | 39 | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized |
|
40 | 40 | # @param [optional, Hash] html_options Options passed to link_to |
|
41 | 41 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to |
|
42 | 42 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
|
43 | 43 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
|
44 | 44 | end |
|
45 | 45 | |
|
46 | 46 | # Displays a link to user's account page if active |
|
47 | 47 | def link_to_user(user, options={}) |
|
48 | 48 | if user.is_a?(User) |
|
49 | 49 | name = h(user.name(options[:format])) |
|
50 | 50 | if user.active? || (User.current.admin? && user.logged?) |
|
51 | 51 | link_to name, user_path(user), :class => user.css_classes |
|
52 | 52 | else |
|
53 | 53 | name |
|
54 | 54 | end |
|
55 | 55 | else |
|
56 | 56 | h(user.to_s) |
|
57 | 57 | end |
|
58 | 58 | end |
|
59 | 59 | |
|
60 | 60 | # Displays a link to +issue+ with its subject. |
|
61 | 61 | # Examples: |
|
62 | 62 | # |
|
63 | 63 | # link_to_issue(issue) # => Defect #6: This is the subject |
|
64 | 64 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... |
|
65 | 65 | # link_to_issue(issue, :subject => false) # => Defect #6 |
|
66 | 66 | # link_to_issue(issue, :project => true) # => Foo - Defect #6 |
|
67 | 67 | # link_to_issue(issue, :subject => false, :tracker => false) # => #6 |
|
68 | 68 | # |
|
69 | 69 | def link_to_issue(issue, options={}) |
|
70 | 70 | title = nil |
|
71 | 71 | subject = nil |
|
72 | 72 | text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" |
|
73 | 73 | if options[:subject] == false |
|
74 | 74 | title = truncate(issue.subject, :length => 60) |
|
75 | 75 | else |
|
76 | 76 | subject = issue.subject |
|
77 | 77 | if options[:truncate] |
|
78 | 78 | subject = truncate(subject, :length => options[:truncate]) |
|
79 | 79 | end |
|
80 | 80 | end |
|
81 | 81 | s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title |
|
82 | 82 | s << h(": #{subject}") if subject |
|
83 | 83 | s = h("#{issue.project} - ") + s if options[:project] |
|
84 | 84 | s |
|
85 | 85 | end |
|
86 | 86 | |
|
87 | 87 | # Generates a link to an attachment. |
|
88 | 88 | # Options: |
|
89 | 89 | # * :text - Link text (default to attachment filename) |
|
90 | 90 | # * :download - Force download (default: false) |
|
91 | 91 | def link_to_attachment(attachment, options={}) |
|
92 | 92 | text = options.delete(:text) || attachment.filename |
|
93 | 93 | action = options.delete(:download) ? 'download' : 'show' |
|
94 | 94 | opt_only_path = {} |
|
95 | 95 | opt_only_path[:only_path] = (options[:only_path] == false ? false : true) |
|
96 | 96 | options.delete(:only_path) |
|
97 | 97 | link_to(h(text), |
|
98 | 98 | {:controller => 'attachments', :action => action, |
|
99 | 99 | :id => attachment, :filename => attachment.filename}.merge(opt_only_path), |
|
100 | 100 | options) |
|
101 | 101 | end |
|
102 | 102 | |
|
103 | 103 | # Generates a link to a SCM revision |
|
104 | 104 | # Options: |
|
105 | 105 | # * :text - Link text (default to the formatted revision) |
|
106 | 106 | def link_to_revision(revision, repository, options={}) |
|
107 | 107 | if repository.is_a?(Project) |
|
108 | 108 | repository = repository.repository |
|
109 | 109 | end |
|
110 | 110 | text = options.delete(:text) || format_revision(revision) |
|
111 | 111 | rev = revision.respond_to?(:identifier) ? revision.identifier : revision |
|
112 | 112 | link_to( |
|
113 | 113 | h(text), |
|
114 | 114 | {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, |
|
115 | 115 | :title => l(:label_revision_id, format_revision(revision)) |
|
116 | 116 | ) |
|
117 | 117 | end |
|
118 | 118 | |
|
119 | 119 | # Generates a link to a message |
|
120 | 120 | def link_to_message(message, options={}, html_options = nil) |
|
121 | 121 | link_to( |
|
122 | 122 | h(truncate(message.subject, :length => 60)), |
|
123 | 123 | { :controller => 'messages', :action => 'show', |
|
124 | 124 | :board_id => message.board_id, |
|
125 | 125 | :id => (message.parent_id || message.id), |
|
126 | 126 | :r => (message.parent_id && message.id), |
|
127 | 127 | :anchor => (message.parent_id ? "message-#{message.id}" : nil) |
|
128 | 128 | }.merge(options), |
|
129 | 129 | html_options |
|
130 | 130 | ) |
|
131 | 131 | end |
|
132 | 132 | |
|
133 | 133 | # Generates a link to a project if active |
|
134 | 134 | # Examples: |
|
135 | 135 | # |
|
136 | 136 | # link_to_project(project) # => link to the specified project overview |
|
137 | 137 | # link_to_project(project, :action=>'settings') # => link to project settings |
|
138 | 138 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options |
|
139 | 139 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) |
|
140 | 140 | # |
|
141 | 141 | def link_to_project(project, options={}, html_options = nil) |
|
142 | 142 | if project.archived? |
|
143 | 143 | h(project) |
|
144 | 144 | else |
|
145 | 145 | url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) |
|
146 | 146 | link_to(h(project), url, html_options) |
|
147 | 147 | end |
|
148 | 148 | end |
|
149 | 149 | |
|
150 | 150 | def wiki_page_path(page, options={}) |
|
151 | 151 | url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options)) |
|
152 | 152 | end |
|
153 | 153 | |
|
154 | 154 | def thumbnail_tag(attachment) |
|
155 | 155 | link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), |
|
156 | 156 | {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, |
|
157 | 157 | :title => attachment.filename |
|
158 | 158 | end |
|
159 | 159 | |
|
160 | 160 | def toggle_link(name, id, options={}) |
|
161 | 161 | onclick = "$('##{id}').toggle(); " |
|
162 | 162 | onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") |
|
163 | 163 | onclick << "return false;" |
|
164 | 164 | link_to(name, "#", :onclick => onclick) |
|
165 | 165 | end |
|
166 | 166 | |
|
167 | 167 | def image_to_function(name, function, html_options = {}) |
|
168 | 168 | html_options.symbolize_keys! |
|
169 | 169 | tag(:input, html_options.merge({ |
|
170 | 170 | :type => "image", :src => image_path(name), |
|
171 | 171 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" |
|
172 | 172 | })) |
|
173 | 173 | end |
|
174 | 174 | |
|
175 | 175 | def format_activity_title(text) |
|
176 | 176 | h(truncate_single_line(text, :length => 100)) |
|
177 | 177 | end |
|
178 | 178 | |
|
179 | 179 | def format_activity_day(date) |
|
180 | 180 | date == User.current.today ? l(:label_today).titleize : format_date(date) |
|
181 | 181 | end |
|
182 | 182 | |
|
183 | 183 | def format_activity_description(text) |
|
184 | 184 | h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') |
|
185 | 185 | ).gsub(/[\r\n]+/, "<br />").html_safe |
|
186 | 186 | end |
|
187 | 187 | |
|
188 | 188 | def format_version_name(version) |
|
189 | 189 | if version.project == @project |
|
190 | 190 | h(version) |
|
191 | 191 | else |
|
192 | 192 | h("#{version.project} - #{version}") |
|
193 | 193 | end |
|
194 | 194 | end |
|
195 | 195 | |
|
196 | 196 | def due_date_distance_in_words(date) |
|
197 | 197 | if date |
|
198 | 198 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
|
199 | 199 | end |
|
200 | 200 | end |
|
201 | 201 | |
|
202 | 202 | # Renders a tree of projects as a nested set of unordered lists |
|
203 | 203 | # The given collection may be a subset of the whole project tree |
|
204 | 204 | # (eg. some intermediate nodes are private and can not be seen) |
|
205 | 205 | def render_project_nested_lists(projects) |
|
206 | 206 | s = '' |
|
207 | 207 | if projects.any? |
|
208 | 208 | ancestors = [] |
|
209 | 209 | original_project = @project |
|
210 | 210 | projects.sort_by(&:lft).each do |project| |
|
211 | 211 | # set the project environment to please macros. |
|
212 | 212 | @project = project |
|
213 | 213 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
|
214 | 214 | s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" |
|
215 | 215 | else |
|
216 | 216 | ancestors.pop |
|
217 | 217 | s << "</li>" |
|
218 | 218 | while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
219 | 219 | ancestors.pop |
|
220 | 220 | s << "</ul></li>\n" |
|
221 | 221 | end |
|
222 | 222 | end |
|
223 | 223 | classes = (ancestors.empty? ? 'root' : 'child') |
|
224 | 224 | s << "<li class='#{classes}'><div class='#{classes}'>" |
|
225 | 225 | s << h(block_given? ? yield(project) : project.name) |
|
226 | 226 | s << "</div>\n" |
|
227 | 227 | ancestors << project |
|
228 | 228 | end |
|
229 | 229 | s << ("</li></ul>\n" * ancestors.size) |
|
230 | 230 | @project = original_project |
|
231 | 231 | end |
|
232 | 232 | s.html_safe |
|
233 | 233 | end |
|
234 | 234 | |
|
235 | 235 | def render_page_hierarchy(pages, node=nil, options={}) |
|
236 | 236 | content = '' |
|
237 | 237 | if pages[node] |
|
238 | 238 | content << "<ul class=\"pages-hierarchy\">\n" |
|
239 | 239 | pages[node].each do |page| |
|
240 | 240 | content << "<li>" |
|
241 | 241 | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil}, |
|
242 | 242 | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
|
243 | 243 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
|
244 | 244 | content << "</li>\n" |
|
245 | 245 | end |
|
246 | 246 | content << "</ul>\n" |
|
247 | 247 | end |
|
248 | 248 | content.html_safe |
|
249 | 249 | end |
|
250 | 250 | |
|
251 | 251 | # Renders flash messages |
|
252 | 252 | def render_flash_messages |
|
253 | 253 | s = '' |
|
254 | 254 | flash.each do |k,v| |
|
255 | 255 | s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") |
|
256 | 256 | end |
|
257 | 257 | s.html_safe |
|
258 | 258 | end |
|
259 | 259 | |
|
260 | 260 | # Renders tabs and their content |
|
261 | 261 | def render_tabs(tabs) |
|
262 | 262 | if tabs.any? |
|
263 | 263 | render :partial => 'common/tabs', :locals => {:tabs => tabs} |
|
264 | 264 | else |
|
265 | 265 | content_tag 'p', l(:label_no_data), :class => "nodata" |
|
266 | 266 | end |
|
267 | 267 | end |
|
268 | 268 | |
|
269 | 269 | # Renders the project quick-jump box |
|
270 | 270 | def render_project_jump_box |
|
271 | 271 | return unless User.current.logged? |
|
272 | 272 | projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq |
|
273 | 273 | if projects.any? |
|
274 | 274 | options = |
|
275 | 275 | ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" + |
|
276 | 276 | '<option value="" disabled="disabled">---</option>').html_safe |
|
277 | 277 | |
|
278 | 278 | options << project_tree_options_for_select(projects, :selected => @project) do |p| |
|
279 | 279 | { :value => project_path(:id => p, :jump => current_menu_item) } |
|
280 | 280 | end |
|
281 | 281 | |
|
282 | 282 | select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') |
|
283 | 283 | end |
|
284 | 284 | end |
|
285 | 285 | |
|
286 | 286 | def project_tree_options_for_select(projects, options = {}) |
|
287 | 287 | s = '' |
|
288 | 288 | project_tree(projects) do |project, level| |
|
289 | 289 | name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe |
|
290 | 290 | tag_options = {:value => project.id} |
|
291 | 291 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
|
292 | 292 | tag_options[:selected] = 'selected' |
|
293 | 293 | else |
|
294 | 294 | tag_options[:selected] = nil |
|
295 | 295 | end |
|
296 | 296 | tag_options.merge!(yield(project)) if block_given? |
|
297 | 297 | s << content_tag('option', name_prefix + h(project), tag_options) |
|
298 | 298 | end |
|
299 | 299 | s.html_safe |
|
300 | 300 | end |
|
301 | 301 | |
|
302 | 302 | # Yields the given block for each project with its level in the tree |
|
303 | 303 | # |
|
304 | 304 | # Wrapper for Project#project_tree |
|
305 | 305 | def project_tree(projects, &block) |
|
306 | 306 | Project.project_tree(projects, &block) |
|
307 | 307 | end |
|
308 | 308 | |
|
309 | 309 | def principals_check_box_tags(name, principals) |
|
310 | 310 | s = '' |
|
311 | 311 | principals.sort.each do |principal| |
|
312 | 312 | s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n" |
|
313 | 313 | end |
|
314 | 314 | s.html_safe |
|
315 | 315 | end |
|
316 | 316 | |
|
317 | 317 | # Returns a string for users/groups option tags |
|
318 | 318 | def principals_options_for_select(collection, selected=nil) |
|
319 | 319 | s = '' |
|
320 | 320 | if collection.include?(User.current) |
|
321 | 321 | s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) |
|
322 | 322 | end |
|
323 | 323 | groups = '' |
|
324 | 324 | collection.sort.each do |element| |
|
325 | 325 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) |
|
326 | 326 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) |
|
327 | 327 | end |
|
328 | 328 | unless groups.empty? |
|
329 | 329 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>) |
|
330 | 330 | end |
|
331 | 331 | s.html_safe |
|
332 | 332 | end |
|
333 | 333 | |
|
334 | 334 | # Options for the new membership projects combo-box |
|
335 | 335 | def options_for_membership_project_select(principal, projects) |
|
336 | 336 | options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") |
|
337 | 337 | options << project_tree_options_for_select(projects) do |p| |
|
338 | 338 | {:disabled => principal.projects.include?(p)} |
|
339 | 339 | end |
|
340 | 340 | options |
|
341 | 341 | end |
|
342 | 342 | |
|
343 | 343 | # Truncates and returns the string as a single line |
|
344 | 344 | def truncate_single_line(string, *args) |
|
345 | 345 | truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') |
|
346 | 346 | end |
|
347 | 347 | |
|
348 | 348 | # Truncates at line break after 250 characters or options[:length] |
|
349 | 349 | def truncate_lines(string, options={}) |
|
350 | 350 | length = options[:length] || 250 |
|
351 | 351 | if string.to_s =~ /\A(.{#{length}}.*?)$/m |
|
352 | 352 | "#{$1}..." |
|
353 | 353 | else |
|
354 | 354 | string |
|
355 | 355 | end |
|
356 | 356 | end |
|
357 | 357 | |
|
358 | 358 | def anchor(text) |
|
359 | 359 | text.to_s.gsub(' ', '_') |
|
360 | 360 | end |
|
361 | 361 | |
|
362 | 362 | def html_hours(text) |
|
363 | 363 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe |
|
364 | 364 | end |
|
365 | 365 | |
|
366 | 366 | def authoring(created, author, options={}) |
|
367 | 367 | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
|
368 | 368 | end |
|
369 | 369 | |
|
370 | 370 | def time_tag(time) |
|
371 | 371 | text = distance_of_time_in_words(Time.now, time) |
|
372 | 372 | if @project |
|
373 | 373 | link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) |
|
374 | 374 | else |
|
375 | 375 | content_tag('acronym', text, :title => format_time(time)) |
|
376 | 376 | end |
|
377 | 377 | end |
|
378 | 378 | |
|
379 | 379 | def syntax_highlight_lines(name, content) |
|
380 | 380 | lines = [] |
|
381 | 381 | syntax_highlight(name, content).each_line { |line| lines << line } |
|
382 | 382 | lines |
|
383 | 383 | end |
|
384 | 384 | |
|
385 | 385 | def syntax_highlight(name, content) |
|
386 | 386 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
|
387 | 387 | end |
|
388 | 388 | |
|
389 | 389 | def to_path_param(path) |
|
390 | 390 | str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") |
|
391 | 391 | str.blank? ? nil : str |
|
392 | 392 | end |
|
393 | 393 | |
|
394 | 394 | def pagination_links_full(paginator, count=nil, options={}) |
|
395 | 395 | page_param = options.delete(:page_param) || :page |
|
396 | 396 | per_page_links = options.delete(:per_page_links) |
|
397 | 397 | url_param = params.dup |
|
398 | 398 | |
|
399 | 399 | html = '' |
|
400 | 400 | if paginator.current.previous |
|
401 | 401 | # \xc2\xab(utf-8) = « |
|
402 | 402 | html << link_to_content_update( |
|
403 | 403 | "\xc2\xab " + l(:label_previous), |
|
404 | 404 | url_param.merge(page_param => paginator.current.previous)) + ' ' |
|
405 | 405 | end |
|
406 | 406 | |
|
407 | 407 | html << (pagination_links_each(paginator, options) do |n| |
|
408 | 408 | link_to_content_update(n.to_s, url_param.merge(page_param => n)) |
|
409 | 409 | end || '') |
|
410 | 410 | |
|
411 | 411 | if paginator.current.next |
|
412 | 412 | # \xc2\xbb(utf-8) = » |
|
413 | 413 | html << ' ' + link_to_content_update( |
|
414 | 414 | (l(:label_next) + " \xc2\xbb"), |
|
415 | 415 | url_param.merge(page_param => paginator.current.next)) |
|
416 | 416 | end |
|
417 | 417 | |
|
418 | 418 | unless count.nil? |
|
419 | 419 | html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" |
|
420 | 420 | if per_page_links != false && links = per_page_links(paginator.items_per_page, count) |
|
421 | 421 | html << " | #{links}" |
|
422 | 422 | end |
|
423 | 423 | end |
|
424 | 424 | |
|
425 | 425 | html.html_safe |
|
426 | 426 | end |
|
427 | 427 | |
|
428 | 428 | def per_page_links(selected=nil, item_count=nil) |
|
429 | 429 | values = Setting.per_page_options_array |
|
430 | 430 | if item_count && values.any? |
|
431 | 431 | if item_count > values.first |
|
432 | 432 | max = values.detect {|value| value >= item_count} || item_count |
|
433 | 433 | else |
|
434 | 434 | max = item_count |
|
435 | 435 | end |
|
436 | 436 | values = values.select {|value| value <= max || value == selected} |
|
437 | 437 | end |
|
438 | 438 | if values.empty? || (values.size == 1 && values.first == selected) |
|
439 | 439 | return nil |
|
440 | 440 | end |
|
441 | 441 | links = values.collect do |n| |
|
442 | 442 | n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) |
|
443 | 443 | end |
|
444 | 444 | l(:label_display_per_page, links.join(', ')) |
|
445 | 445 | end |
|
446 | 446 | |
|
447 | 447 | def reorder_links(name, url, method = :post) |
|
448 | 448 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), |
|
449 | 449 | url.merge({"#{name}[move_to]" => 'highest'}), |
|
450 | 450 | :method => method, :title => l(:label_sort_highest)) + |
|
451 | 451 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), |
|
452 | 452 | url.merge({"#{name}[move_to]" => 'higher'}), |
|
453 | 453 | :method => method, :title => l(:label_sort_higher)) + |
|
454 | 454 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), |
|
455 | 455 | url.merge({"#{name}[move_to]" => 'lower'}), |
|
456 | 456 | :method => method, :title => l(:label_sort_lower)) + |
|
457 | 457 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), |
|
458 | 458 | url.merge({"#{name}[move_to]" => 'lowest'}), |
|
459 | 459 | :method => method, :title => l(:label_sort_lowest)) |
|
460 | 460 | end |
|
461 | 461 | |
|
462 | 462 | def breadcrumb(*args) |
|
463 | 463 | elements = args.flatten |
|
464 | 464 | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil |
|
465 | 465 | end |
|
466 | 466 | |
|
467 | 467 | def other_formats_links(&block) |
|
468 | 468 | concat('<p class="other-formats">'.html_safe + l(:label_export_to)) |
|
469 | 469 | yield Redmine::Views::OtherFormatsBuilder.new(self) |
|
470 | 470 | concat('</p>'.html_safe) |
|
471 | 471 | end |
|
472 | 472 | |
|
473 | 473 | def page_header_title |
|
474 | 474 | if @project.nil? || @project.new_record? |
|
475 | 475 | h(Setting.app_title) |
|
476 | 476 | else |
|
477 | 477 | b = [] |
|
478 | 478 | ancestors = (@project.root? ? [] : @project.ancestors.visible.all) |
|
479 | 479 | if ancestors.any? |
|
480 | 480 | root = ancestors.shift |
|
481 | 481 | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') |
|
482 | 482 | if ancestors.size > 2 |
|
483 | 483 | b << "\xe2\x80\xa6" |
|
484 | 484 | ancestors = ancestors[-2, 2] |
|
485 | 485 | end |
|
486 | 486 | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } |
|
487 | 487 | end |
|
488 | 488 | b << h(@project) |
|
489 | 489 | b.join(" \xc2\xbb ").html_safe |
|
490 | 490 | end |
|
491 | 491 | end |
|
492 | 492 | |
|
493 | 493 | def html_title(*args) |
|
494 | 494 | if args.empty? |
|
495 | 495 | title = @html_title || [] |
|
496 | 496 | title << @project.name if @project |
|
497 | 497 | title << Setting.app_title unless Setting.app_title == title.last |
|
498 | 498 | title.select {|t| !t.blank? }.join(' - ') |
|
499 | 499 | else |
|
500 | 500 | @html_title ||= [] |
|
501 | 501 | @html_title += args |
|
502 | 502 | end |
|
503 | 503 | end |
|
504 | 504 | |
|
505 | 505 | # Returns the theme, controller name, and action as css classes for the |
|
506 | 506 | # HTML body. |
|
507 | 507 | def body_css_classes |
|
508 | 508 | css = [] |
|
509 | 509 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
|
510 | 510 | css << 'theme-' + theme.name |
|
511 | 511 | end |
|
512 | 512 | |
|
513 | 513 | css << 'controller-' + controller_name |
|
514 | 514 | css << 'action-' + action_name |
|
515 | 515 | css.join(' ') |
|
516 | 516 | end |
|
517 | 517 | |
|
518 | 518 | def accesskey(s) |
|
519 | 519 | Redmine::AccessKeys.key_for s |
|
520 | 520 | end |
|
521 | 521 | |
|
522 | 522 | # Formats text according to system settings. |
|
523 | 523 | # 2 ways to call this method: |
|
524 | 524 | # * with a String: textilizable(text, options) |
|
525 | 525 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
|
526 | 526 | def textilizable(*args) |
|
527 | 527 | options = args.last.is_a?(Hash) ? args.pop : {} |
|
528 | 528 | case args.size |
|
529 | 529 | when 1 |
|
530 | 530 | obj = options[:object] |
|
531 | 531 | text = args.shift |
|
532 | 532 | when 2 |
|
533 | 533 | obj = args.shift |
|
534 | 534 | attr = args.shift |
|
535 | 535 | text = obj.send(attr).to_s |
|
536 | 536 | else |
|
537 | 537 | raise ArgumentError, 'invalid arguments to textilizable' |
|
538 | 538 | end |
|
539 | 539 | return '' if text.blank? |
|
540 | 540 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
|
541 | 541 | only_path = options.delete(:only_path) == false ? false : true |
|
542 | 542 | |
|
543 | 543 | text = text.dup |
|
544 | 544 | macros = catch_macros(text) |
|
545 | 545 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
|
546 | 546 | |
|
547 | 547 | @parsed_headings = [] |
|
548 | 548 | @heading_anchors = {} |
|
549 | 549 | @current_section = 0 if options[:edit_section_links] |
|
550 | 550 | |
|
551 | 551 | parse_sections(text, project, obj, attr, only_path, options) |
|
552 | 552 | text = parse_non_pre_blocks(text, obj, macros) do |text| |
|
553 | 553 | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| |
|
554 | 554 | send method_name, text, project, obj, attr, only_path, options |
|
555 | 555 | end |
|
556 | 556 | end |
|
557 | 557 | parse_headings(text, project, obj, attr, only_path, options) |
|
558 | 558 | |
|
559 | 559 | if @parsed_headings.any? |
|
560 | 560 | replace_toc(text, @parsed_headings) |
|
561 | 561 | end |
|
562 | 562 | |
|
563 | 563 | text.html_safe |
|
564 | 564 | end |
|
565 | 565 | |
|
566 | 566 | def parse_non_pre_blocks(text, obj, macros) |
|
567 | 567 | s = StringScanner.new(text) |
|
568 | 568 | tags = [] |
|
569 | 569 | parsed = '' |
|
570 | 570 | while !s.eos? |
|
571 | 571 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
|
572 | 572 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
|
573 | 573 | if tags.empty? |
|
574 | 574 | yield text |
|
575 | 575 | inject_macros(text, obj, macros) if macros.any? |
|
576 | 576 | else |
|
577 | 577 | inject_macros(text, obj, macros, false) if macros.any? |
|
578 | 578 | end |
|
579 | 579 | parsed << text |
|
580 | 580 | if tag |
|
581 | 581 | if closing |
|
582 | 582 | if tags.last == tag.downcase |
|
583 | 583 | tags.pop |
|
584 | 584 | end |
|
585 | 585 | else |
|
586 | 586 | tags << tag.downcase |
|
587 | 587 | end |
|
588 | 588 | parsed << full_tag |
|
589 | 589 | end |
|
590 | 590 | end |
|
591 | 591 | # Close any non closing tags |
|
592 | 592 | while tag = tags.pop |
|
593 | 593 | parsed << "</#{tag}>" |
|
594 | 594 | end |
|
595 | 595 | parsed |
|
596 | 596 | end |
|
597 | 597 | |
|
598 | 598 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
|
599 | 599 | # when using an image link, try to use an attachment, if possible |
|
600 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) | |
|
601 |
attachments = options[:attachments] || |
|
|
600 | if options[:attachments].present? || (obj && obj.respond_to?(:attachments)) | |
|
601 | attachments = options[:attachments] || [] | |
|
602 | attachments += obj.attachments if obj | |
|
602 | 603 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
|
603 | 604 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
|
604 | 605 | # search for the picture in attachments |
|
605 | 606 | if found = Attachment.latest_attach(attachments, filename) |
|
606 | 607 | image_url = url_for :only_path => only_path, :controller => 'attachments', |
|
607 | 608 | :action => 'download', :id => found |
|
608 | 609 | desc = found.description.to_s.gsub('"', '') |
|
609 | 610 | if !desc.blank? && alttext.blank? |
|
610 | 611 | alt = " title=\"#{desc}\" alt=\"#{desc}\"" |
|
611 | 612 | end |
|
612 | 613 | "src=\"#{image_url}\"#{alt}" |
|
613 | 614 | else |
|
614 | 615 | m |
|
615 | 616 | end |
|
616 | 617 | end |
|
617 | 618 | end |
|
618 | 619 | end |
|
619 | 620 | |
|
620 | 621 | # Wiki links |
|
621 | 622 | # |
|
622 | 623 | # Examples: |
|
623 | 624 | # [[mypage]] |
|
624 | 625 | # [[mypage|mytext]] |
|
625 | 626 | # wiki links can refer other project wikis, using project name or identifier: |
|
626 | 627 | # [[project:]] -> wiki starting page |
|
627 | 628 | # [[project:|mytext]] |
|
628 | 629 | # [[project:mypage]] |
|
629 | 630 | # [[project:mypage|mytext]] |
|
630 | 631 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
|
631 | 632 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
|
632 | 633 | link_project = project |
|
633 | 634 | esc, all, page, title = $1, $2, $3, $5 |
|
634 | 635 | if esc.nil? |
|
635 | 636 | if page =~ /^([^\:]+)\:(.*)$/ |
|
636 | 637 | link_project = Project.find_by_identifier($1) || Project.find_by_name($1) |
|
637 | 638 | page = $2 |
|
638 | 639 | title ||= $1 if page.blank? |
|
639 | 640 | end |
|
640 | 641 | |
|
641 | 642 | if link_project && link_project.wiki |
|
642 | 643 | # extract anchor |
|
643 | 644 | anchor = nil |
|
644 | 645 | if page =~ /^(.+?)\#(.+)$/ |
|
645 | 646 | page, anchor = $1, $2 |
|
646 | 647 | end |
|
647 | 648 | anchor = sanitize_anchor_name(anchor) if anchor.present? |
|
648 | 649 | # check if page exists |
|
649 | 650 | wiki_page = link_project.wiki.find_page(page) |
|
650 | 651 | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
|
651 | 652 | "##{anchor}" |
|
652 | 653 | else |
|
653 | 654 | case options[:wiki_links] |
|
654 | 655 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') |
|
655 | 656 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export |
|
656 | 657 | else |
|
657 | 658 | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
|
658 | 659 | parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil |
|
659 | 660 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, |
|
660 | 661 | :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent) |
|
661 | 662 | end |
|
662 | 663 | end |
|
663 | 664 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
|
664 | 665 | else |
|
665 | 666 | # project or wiki doesn't exist |
|
666 | 667 | all |
|
667 | 668 | end |
|
668 | 669 | else |
|
669 | 670 | all |
|
670 | 671 | end |
|
671 | 672 | end |
|
672 | 673 | end |
|
673 | 674 | |
|
674 | 675 | # Redmine links |
|
675 | 676 | # |
|
676 | 677 | # Examples: |
|
677 | 678 | # Issues: |
|
678 | 679 | # #52 -> Link to issue #52 |
|
679 | 680 | # Changesets: |
|
680 | 681 | # r52 -> Link to revision 52 |
|
681 | 682 | # commit:a85130f -> Link to scmid starting with a85130f |
|
682 | 683 | # Documents: |
|
683 | 684 | # document#17 -> Link to document with id 17 |
|
684 | 685 | # document:Greetings -> Link to the document with title "Greetings" |
|
685 | 686 | # document:"Some document" -> Link to the document with title "Some document" |
|
686 | 687 | # Versions: |
|
687 | 688 | # version#3 -> Link to version with id 3 |
|
688 | 689 | # version:1.0.0 -> Link to version named "1.0.0" |
|
689 | 690 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
|
690 | 691 | # Attachments: |
|
691 | 692 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
|
692 | 693 | # Source files: |
|
693 | 694 | # source:some/file -> Link to the file located at /some/file in the project's repository |
|
694 | 695 | # source:some/file@52 -> Link to the file's revision 52 |
|
695 | 696 | # source:some/file#L120 -> Link to line 120 of the file |
|
696 | 697 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
|
697 | 698 | # export:some/file -> Force the download of the file |
|
698 | 699 | # Forum messages: |
|
699 | 700 | # message#1218 -> Link to message with id 1218 |
|
700 | 701 | # |
|
701 | 702 | # Links can refer other objects from other projects, using project identifier: |
|
702 | 703 | # identifier:r52 |
|
703 | 704 | # identifier:document:"Some document" |
|
704 | 705 | # identifier:version:1.0.0 |
|
705 | 706 | # identifier:source:some/file |
|
706 | 707 | def parse_redmine_links(text, project, obj, attr, only_path, options) |
|
707 | 708 | text.gsub!(%r{([\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| |
|
708 | 709 | leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17 |
|
709 | 710 | link = nil |
|
710 | 711 | if project_identifier |
|
711 | 712 | project = Project.visible.find_by_identifier(project_identifier) |
|
712 | 713 | end |
|
713 | 714 | if esc.nil? |
|
714 | 715 | if prefix.nil? && sep == 'r' |
|
715 | 716 | if project |
|
716 | 717 | repository = nil |
|
717 | 718 | if repo_identifier |
|
718 | 719 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
719 | 720 | else |
|
720 | 721 | repository = project.repository |
|
721 | 722 | end |
|
722 | 723 | # project.changesets.visible raises an SQL error because of a double join on repositories |
|
723 | 724 | if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) |
|
724 | 725 | link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision}, |
|
725 | 726 | :class => 'changeset', |
|
726 | 727 | :title => truncate_single_line(changeset.comments, :length => 100)) |
|
727 | 728 | end |
|
728 | 729 | end |
|
729 | 730 | elsif sep == '#' |
|
730 | 731 | oid = identifier.to_i |
|
731 | 732 | case prefix |
|
732 | 733 | when nil |
|
733 | 734 | if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) |
|
734 | 735 | anchor = comment_id ? "note-#{comment_id}" : nil |
|
735 | 736 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, |
|
736 | 737 | :class => issue.css_classes, |
|
737 | 738 | :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") |
|
738 | 739 | end |
|
739 | 740 | when 'document' |
|
740 | 741 | if document = Document.visible.find_by_id(oid) |
|
741 | 742 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
742 | 743 | :class => 'document' |
|
743 | 744 | end |
|
744 | 745 | when 'version' |
|
745 | 746 | if version = Version.visible.find_by_id(oid) |
|
746 | 747 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
747 | 748 | :class => 'version' |
|
748 | 749 | end |
|
749 | 750 | when 'message' |
|
750 | 751 | if message = Message.visible.find_by_id(oid, :include => :parent) |
|
751 | 752 | link = link_to_message(message, {:only_path => only_path}, :class => 'message') |
|
752 | 753 | end |
|
753 | 754 | when 'forum' |
|
754 | 755 | if board = Board.visible.find_by_id(oid) |
|
755 | 756 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
756 | 757 | :class => 'board' |
|
757 | 758 | end |
|
758 | 759 | when 'news' |
|
759 | 760 | if news = News.visible.find_by_id(oid) |
|
760 | 761 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
761 | 762 | :class => 'news' |
|
762 | 763 | end |
|
763 | 764 | when 'project' |
|
764 | 765 | if p = Project.visible.find_by_id(oid) |
|
765 | 766 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
766 | 767 | end |
|
767 | 768 | end |
|
768 | 769 | elsif sep == ':' |
|
769 | 770 | # removes the double quotes if any |
|
770 | 771 | name = identifier.gsub(%r{^"(.*)"$}, "\\1") |
|
771 | 772 | case prefix |
|
772 | 773 | when 'document' |
|
773 | 774 | if project && document = project.documents.visible.find_by_title(name) |
|
774 | 775 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
775 | 776 | :class => 'document' |
|
776 | 777 | end |
|
777 | 778 | when 'version' |
|
778 | 779 | if project && version = project.versions.visible.find_by_name(name) |
|
779 | 780 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
780 | 781 | :class => 'version' |
|
781 | 782 | end |
|
782 | 783 | when 'forum' |
|
783 | 784 | if project && board = project.boards.visible.find_by_name(name) |
|
784 | 785 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
785 | 786 | :class => 'board' |
|
786 | 787 | end |
|
787 | 788 | when 'news' |
|
788 | 789 | if project && news = project.news.visible.find_by_title(name) |
|
789 | 790 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
790 | 791 | :class => 'news' |
|
791 | 792 | end |
|
792 | 793 | when 'commit', 'source', 'export' |
|
793 | 794 | if project |
|
794 | 795 | repository = nil |
|
795 | 796 | if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$} |
|
796 | 797 | repo_prefix, repo_identifier, name = $1, $2, $3 |
|
797 | 798 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
798 | 799 | else |
|
799 | 800 | repository = project.repository |
|
800 | 801 | end |
|
801 | 802 | if prefix == 'commit' |
|
802 | 803 | if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first) |
|
803 | 804 | 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}, |
|
804 | 805 | :class => 'changeset', |
|
805 | 806 | :title => truncate_single_line(h(changeset.comments), :length => 100) |
|
806 | 807 | end |
|
807 | 808 | else |
|
808 | 809 | if repository && User.current.allowed_to?(:browse_repository, project) |
|
809 | 810 | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} |
|
810 | 811 | path, rev, anchor = $1, $3, $5 |
|
811 | 812 | link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param, |
|
812 | 813 | :path => to_path_param(path), |
|
813 | 814 | :rev => rev, |
|
814 | 815 | :anchor => anchor}, |
|
815 | 816 | :class => (prefix == 'export' ? 'source download' : 'source') |
|
816 | 817 | end |
|
817 | 818 | end |
|
818 | 819 | repo_prefix = nil |
|
819 | 820 | end |
|
820 | 821 | when 'attachment' |
|
821 | 822 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
|
822 | 823 | if attachments && attachment = attachments.detect {|a| a.filename == name } |
|
823 | 824 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, |
|
824 | 825 | :class => 'attachment' |
|
825 | 826 | end |
|
826 | 827 | when 'project' |
|
827 | 828 | if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first |
|
828 | 829 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
829 | 830 | end |
|
830 | 831 | end |
|
831 | 832 | end |
|
832 | 833 | end |
|
833 | 834 | (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) |
|
834 | 835 | end |
|
835 | 836 | end |
|
836 | 837 | |
|
837 | 838 | HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) |
|
838 | 839 | |
|
839 | 840 | def parse_sections(text, project, obj, attr, only_path, options) |
|
840 | 841 | return unless options[:edit_section_links] |
|
841 | 842 | text.gsub!(HEADING_RE) do |
|
842 | 843 | heading = $1 |
|
843 | 844 | @current_section += 1 |
|
844 | 845 | if @current_section > 1 |
|
845 | 846 | content_tag('div', |
|
846 | 847 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), |
|
847 | 848 | :class => 'contextual', |
|
848 | 849 | :title => l(:button_edit_section)) + heading.html_safe |
|
849 | 850 | else |
|
850 | 851 | heading |
|
851 | 852 | end |
|
852 | 853 | end |
|
853 | 854 | end |
|
854 | 855 | |
|
855 | 856 | # Headings and TOC |
|
856 | 857 | # Adds ids and links to headings unless options[:headings] is set to false |
|
857 | 858 | def parse_headings(text, project, obj, attr, only_path, options) |
|
858 | 859 | return if options[:headings] == false |
|
859 | 860 | |
|
860 | 861 | text.gsub!(HEADING_RE) do |
|
861 | 862 | level, attrs, content = $2.to_i, $3, $4 |
|
862 | 863 | item = strip_tags(content).strip |
|
863 | 864 | anchor = sanitize_anchor_name(item) |
|
864 | 865 | # used for single-file wiki export |
|
865 | 866 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) |
|
866 | 867 | @heading_anchors[anchor] ||= 0 |
|
867 | 868 | idx = (@heading_anchors[anchor] += 1) |
|
868 | 869 | if idx > 1 |
|
869 | 870 | anchor = "#{anchor}-#{idx}" |
|
870 | 871 | end |
|
871 | 872 | @parsed_headings << [level, anchor, item] |
|
872 | 873 | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>" |
|
873 | 874 | end |
|
874 | 875 | end |
|
875 | 876 | |
|
876 | 877 | MACROS_RE = /( |
|
877 | 878 | (!)? # escaping |
|
878 | 879 | ( |
|
879 | 880 | \{\{ # opening tag |
|
880 | 881 | ([\w]+) # macro name |
|
881 | 882 | (\(([^\n\r]*?)\))? # optional arguments |
|
882 | 883 | ([\n\r].*?[\n\r])? # optional block of text |
|
883 | 884 | \}\} # closing tag |
|
884 | 885 | ) |
|
885 | 886 | )/mx unless const_defined?(:MACROS_RE) |
|
886 | 887 | |
|
887 | 888 | MACRO_SUB_RE = /( |
|
888 | 889 | \{\{ |
|
889 | 890 | macro\((\d+)\) |
|
890 | 891 | \}\} |
|
891 | 892 | )/x unless const_defined?(:MACRO_SUB_RE) |
|
892 | 893 | |
|
893 | 894 | # Extracts macros from text |
|
894 | 895 | def catch_macros(text) |
|
895 | 896 | macros = {} |
|
896 | 897 | text.gsub!(MACROS_RE) do |
|
897 | 898 | all, macro = $1, $4.downcase |
|
898 | 899 | if macro_exists?(macro) || all =~ MACRO_SUB_RE |
|
899 | 900 | index = macros.size |
|
900 | 901 | macros[index] = all |
|
901 | 902 | "{{macro(#{index})}}" |
|
902 | 903 | else |
|
903 | 904 | all |
|
904 | 905 | end |
|
905 | 906 | end |
|
906 | 907 | macros |
|
907 | 908 | end |
|
908 | 909 | |
|
909 | 910 | # Executes and replaces macros in text |
|
910 | 911 | def inject_macros(text, obj, macros, execute=true) |
|
911 | 912 | text.gsub!(MACRO_SUB_RE) do |
|
912 | 913 | all, index = $1, $2.to_i |
|
913 | 914 | orig = macros.delete(index) |
|
914 | 915 | if execute && orig && orig =~ MACROS_RE |
|
915 | 916 | esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) |
|
916 | 917 | if esc.nil? |
|
917 | 918 | h(exec_macro(macro, obj, args, block) || all) |
|
918 | 919 | else |
|
919 | 920 | h(all) |
|
920 | 921 | end |
|
921 | 922 | elsif orig |
|
922 | 923 | h(orig) |
|
923 | 924 | else |
|
924 | 925 | h(all) |
|
925 | 926 | end |
|
926 | 927 | end |
|
927 | 928 | end |
|
928 | 929 | |
|
929 | 930 | TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
|
930 | 931 | |
|
931 | 932 | # Renders the TOC with given headings |
|
932 | 933 | def replace_toc(text, headings) |
|
933 | 934 | text.gsub!(TOC_RE) do |
|
934 | 935 | # Keep only the 4 first levels |
|
935 | 936 | headings = headings.select{|level, anchor, item| level <= 4} |
|
936 | 937 | if headings.empty? |
|
937 | 938 | '' |
|
938 | 939 | else |
|
939 | 940 | div_class = 'toc' |
|
940 | 941 | div_class << ' right' if $1 == '>' |
|
941 | 942 | div_class << ' left' if $1 == '<' |
|
942 | 943 | out = "<ul class=\"#{div_class}\"><li>" |
|
943 | 944 | root = headings.map(&:first).min |
|
944 | 945 | current = root |
|
945 | 946 | started = false |
|
946 | 947 | headings.each do |level, anchor, item| |
|
947 | 948 | if level > current |
|
948 | 949 | out << '<ul><li>' * (level - current) |
|
949 | 950 | elsif level < current |
|
950 | 951 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
|
951 | 952 | elsif started |
|
952 | 953 | out << '</li><li>' |
|
953 | 954 | end |
|
954 | 955 | out << "<a href=\"##{anchor}\">#{item}</a>" |
|
955 | 956 | current = level |
|
956 | 957 | started = true |
|
957 | 958 | end |
|
958 | 959 | out << '</li></ul>' * (current - root) |
|
959 | 960 | out << '</li></ul>' |
|
960 | 961 | end |
|
961 | 962 | end |
|
962 | 963 | end |
|
963 | 964 | |
|
964 | 965 | # Same as Rails' simple_format helper without using paragraphs |
|
965 | 966 | def simple_format_without_paragraph(text) |
|
966 | 967 | text.to_s. |
|
967 | 968 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
|
968 | 969 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
|
969 | 970 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
|
970 | 971 | html_safe |
|
971 | 972 | end |
|
972 | 973 | |
|
973 | 974 | def lang_options_for_select(blank=true) |
|
974 | 975 | (blank ? [["(auto)", ""]] : []) + languages_options |
|
975 | 976 | end |
|
976 | 977 | |
|
977 | 978 | def label_tag_for(name, option_tags = nil, options = {}) |
|
978 | 979 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") |
|
979 | 980 | content_tag("label", label_text) |
|
980 | 981 | end |
|
981 | 982 | |
|
982 | 983 | def labelled_form_for(*args, &proc) |
|
983 | 984 | args << {} unless args.last.is_a?(Hash) |
|
984 | 985 | options = args.last |
|
985 | 986 | if args.first.is_a?(Symbol) |
|
986 | 987 | options.merge!(:as => args.shift) |
|
987 | 988 | end |
|
988 | 989 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
989 | 990 | form_for(*args, &proc) |
|
990 | 991 | end |
|
991 | 992 | |
|
992 | 993 | def labelled_fields_for(*args, &proc) |
|
993 | 994 | args << {} unless args.last.is_a?(Hash) |
|
994 | 995 | options = args.last |
|
995 | 996 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
996 | 997 | fields_for(*args, &proc) |
|
997 | 998 | end |
|
998 | 999 | |
|
999 | 1000 | def labelled_remote_form_for(*args, &proc) |
|
1000 | 1001 | ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." |
|
1001 | 1002 | args << {} unless args.last.is_a?(Hash) |
|
1002 | 1003 | options = args.last |
|
1003 | 1004 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) |
|
1004 | 1005 | form_for(*args, &proc) |
|
1005 | 1006 | end |
|
1006 | 1007 | |
|
1007 | 1008 | def error_messages_for(*objects) |
|
1008 | 1009 | html = "" |
|
1009 | 1010 | objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact |
|
1010 | 1011 | errors = objects.map {|o| o.errors.full_messages}.flatten |
|
1011 | 1012 | if errors.any? |
|
1012 | 1013 | html << "<div id='errorExplanation'><ul>\n" |
|
1013 | 1014 | errors.each do |error| |
|
1014 | 1015 | html << "<li>#{h error}</li>\n" |
|
1015 | 1016 | end |
|
1016 | 1017 | html << "</ul></div>\n" |
|
1017 | 1018 | end |
|
1018 | 1019 | html.html_safe |
|
1019 | 1020 | end |
|
1020 | 1021 | |
|
1021 | 1022 | def delete_link(url, options={}) |
|
1022 | 1023 | options = { |
|
1023 | 1024 | :method => :delete, |
|
1024 | 1025 | :data => {:confirm => l(:text_are_you_sure)}, |
|
1025 | 1026 | :class => 'icon icon-del' |
|
1026 | 1027 | }.merge(options) |
|
1027 | 1028 | |
|
1028 | 1029 | link_to l(:button_delete), url, options |
|
1029 | 1030 | end |
|
1030 | 1031 | |
|
1031 | 1032 | def preview_link(url, form, target='preview', options={}) |
|
1032 | 1033 | content_tag 'a', l(:label_preview), { |
|
1033 | 1034 | :href => "#", |
|
1034 | 1035 | :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, |
|
1035 | 1036 | :accesskey => accesskey(:preview) |
|
1036 | 1037 | }.merge(options) |
|
1037 | 1038 | end |
|
1038 | 1039 | |
|
1039 | 1040 | def link_to_function(name, function, html_options={}) |
|
1040 | 1041 | content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) |
|
1041 | 1042 | end |
|
1042 | 1043 | |
|
1043 | 1044 | # Helper to render JSON in views |
|
1044 | 1045 | def raw_json(arg) |
|
1045 | 1046 | arg.to_json.to_s.gsub('/', '\/').html_safe |
|
1046 | 1047 | end |
|
1047 | 1048 | |
|
1048 | 1049 | def back_url |
|
1049 | 1050 | url = params[:back_url] |
|
1050 | 1051 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
1051 | 1052 | url = CGI.unescape(referer.to_s) |
|
1052 | 1053 | end |
|
1053 | 1054 | url |
|
1054 | 1055 | end |
|
1055 | 1056 | |
|
1056 | 1057 | def back_url_hidden_field_tag |
|
1057 | 1058 | url = back_url |
|
1058 | 1059 | hidden_field_tag('back_url', url, :id => nil) unless url.blank? |
|
1059 | 1060 | end |
|
1060 | 1061 | |
|
1061 | 1062 | def check_all_links(form_name) |
|
1062 | 1063 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
|
1063 | 1064 | " | ".html_safe + |
|
1064 | 1065 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
|
1065 | 1066 | end |
|
1066 | 1067 | |
|
1067 | 1068 | def progress_bar(pcts, options={}) |
|
1068 | 1069 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
|
1069 | 1070 | pcts = pcts.collect(&:round) |
|
1070 | 1071 | pcts[1] = pcts[1] - pcts[0] |
|
1071 | 1072 | pcts << (100 - pcts[1] - pcts[0]) |
|
1072 | 1073 | width = options[:width] || '100px;' |
|
1073 | 1074 | legend = options[:legend] || '' |
|
1074 | 1075 | content_tag('table', |
|
1075 | 1076 | content_tag('tr', |
|
1076 | 1077 | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + |
|
1077 | 1078 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + |
|
1078 | 1079 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) |
|
1079 | 1080 | ), :class => 'progress', :style => "width: #{width};").html_safe + |
|
1080 | 1081 | content_tag('p', legend, :class => 'pourcent').html_safe |
|
1081 | 1082 | end |
|
1082 | 1083 | |
|
1083 | 1084 | def checked_image(checked=true) |
|
1084 | 1085 | if checked |
|
1085 | 1086 | image_tag 'toggle_check.png' |
|
1086 | 1087 | end |
|
1087 | 1088 | end |
|
1088 | 1089 | |
|
1089 | 1090 | def context_menu(url) |
|
1090 | 1091 | unless @context_menu_included |
|
1091 | 1092 | content_for :header_tags do |
|
1092 | 1093 | javascript_include_tag('context_menu') + |
|
1093 | 1094 | stylesheet_link_tag('context_menu') |
|
1094 | 1095 | end |
|
1095 | 1096 | if l(:direction) == 'rtl' |
|
1096 | 1097 | content_for :header_tags do |
|
1097 | 1098 | stylesheet_link_tag('context_menu_rtl') |
|
1098 | 1099 | end |
|
1099 | 1100 | end |
|
1100 | 1101 | @context_menu_included = true |
|
1101 | 1102 | end |
|
1102 | 1103 | javascript_tag "contextMenuInit('#{ url_for(url) }')" |
|
1103 | 1104 | end |
|
1104 | 1105 | |
|
1105 | 1106 | def calendar_for(field_id) |
|
1106 | 1107 | include_calendar_headers_tags |
|
1107 | 1108 | javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") |
|
1108 | 1109 | end |
|
1109 | 1110 | |
|
1110 | 1111 | def include_calendar_headers_tags |
|
1111 | 1112 | unless @calendar_headers_tags_included |
|
1112 | 1113 | @calendar_headers_tags_included = true |
|
1113 | 1114 | content_for :header_tags do |
|
1114 | 1115 | start_of_week = Setting.start_of_week |
|
1115 | 1116 | start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? |
|
1116 | 1117 | # Redmine uses 1..7 (monday..sunday) in settings and locales |
|
1117 | 1118 | # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 |
|
1118 | 1119 | start_of_week = start_of_week.to_i % 7 |
|
1119 | 1120 | |
|
1120 | 1121 | tags = javascript_tag( |
|
1121 | 1122 | "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + |
|
1122 | 1123 | "showOn: 'button', buttonImageOnly: true, buttonImage: '" + |
|
1123 | 1124 | path_to_image('/images/calendar.png') + |
|
1124 | 1125 | "', showButtonPanel: true};") |
|
1125 | 1126 | jquery_locale = l('jquery.locale', :default => current_language.to_s) |
|
1126 | 1127 | unless jquery_locale == 'en' |
|
1127 | 1128 | tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") |
|
1128 | 1129 | end |
|
1129 | 1130 | tags |
|
1130 | 1131 | end |
|
1131 | 1132 | end |
|
1132 | 1133 | end |
|
1133 | 1134 | |
|
1134 | 1135 | # Overrides Rails' stylesheet_link_tag with themes and plugins support. |
|
1135 | 1136 | # Examples: |
|
1136 | 1137 | # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults |
|
1137 | 1138 | # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets |
|
1138 | 1139 | # |
|
1139 | 1140 | def stylesheet_link_tag(*sources) |
|
1140 | 1141 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1141 | 1142 | plugin = options.delete(:plugin) |
|
1142 | 1143 | sources = sources.map do |source| |
|
1143 | 1144 | if plugin |
|
1144 | 1145 | "/plugin_assets/#{plugin}/stylesheets/#{source}" |
|
1145 | 1146 | elsif current_theme && current_theme.stylesheets.include?(source) |
|
1146 | 1147 | current_theme.stylesheet_path(source) |
|
1147 | 1148 | else |
|
1148 | 1149 | source |
|
1149 | 1150 | end |
|
1150 | 1151 | end |
|
1151 | 1152 | super sources, options |
|
1152 | 1153 | end |
|
1153 | 1154 | |
|
1154 | 1155 | # Overrides Rails' image_tag with themes and plugins support. |
|
1155 | 1156 | # Examples: |
|
1156 | 1157 | # image_tag('image.png') # => picks image.png from the current theme or defaults |
|
1157 | 1158 | # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets |
|
1158 | 1159 | # |
|
1159 | 1160 | def image_tag(source, options={}) |
|
1160 | 1161 | if plugin = options.delete(:plugin) |
|
1161 | 1162 | source = "/plugin_assets/#{plugin}/images/#{source}" |
|
1162 | 1163 | elsif current_theme && current_theme.images.include?(source) |
|
1163 | 1164 | source = current_theme.image_path(source) |
|
1164 | 1165 | end |
|
1165 | 1166 | super source, options |
|
1166 | 1167 | end |
|
1167 | 1168 | |
|
1168 | 1169 | # Overrides Rails' javascript_include_tag with plugins support |
|
1169 | 1170 | # Examples: |
|
1170 | 1171 | # javascript_include_tag('scripts') # => picks scripts.js from defaults |
|
1171 | 1172 | # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets |
|
1172 | 1173 | # |
|
1173 | 1174 | def javascript_include_tag(*sources) |
|
1174 | 1175 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1175 | 1176 | if plugin = options.delete(:plugin) |
|
1176 | 1177 | sources = sources.map do |source| |
|
1177 | 1178 | if plugin |
|
1178 | 1179 | "/plugin_assets/#{plugin}/javascripts/#{source}" |
|
1179 | 1180 | else |
|
1180 | 1181 | source |
|
1181 | 1182 | end |
|
1182 | 1183 | end |
|
1183 | 1184 | end |
|
1184 | 1185 | super sources, options |
|
1185 | 1186 | end |
|
1186 | 1187 | |
|
1187 | 1188 | def content_for(name, content = nil, &block) |
|
1188 | 1189 | @has_content ||= {} |
|
1189 | 1190 | @has_content[name] = true |
|
1190 | 1191 | super(name, content, &block) |
|
1191 | 1192 | end |
|
1192 | 1193 | |
|
1193 | 1194 | def has_content?(name) |
|
1194 | 1195 | (@has_content && @has_content[name]) || false |
|
1195 | 1196 | end |
|
1196 | 1197 | |
|
1197 | 1198 | def sidebar_content? |
|
1198 | 1199 | has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? |
|
1199 | 1200 | end |
|
1200 | 1201 | |
|
1201 | 1202 | def view_layouts_base_sidebar_hook_response |
|
1202 | 1203 | @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) |
|
1203 | 1204 | end |
|
1204 | 1205 | |
|
1205 | 1206 | def email_delivery_enabled? |
|
1206 | 1207 | !!ActionMailer::Base.perform_deliveries |
|
1207 | 1208 | end |
|
1208 | 1209 | |
|
1209 | 1210 | # Returns the avatar image tag for the given +user+ if avatars are enabled |
|
1210 | 1211 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') |
|
1211 | 1212 | def avatar(user, options = { }) |
|
1212 | 1213 | if Setting.gravatar_enabled? |
|
1213 | 1214 | options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) |
|
1214 | 1215 | email = nil |
|
1215 | 1216 | if user.respond_to?(:mail) |
|
1216 | 1217 | email = user.mail |
|
1217 | 1218 | elsif user.to_s =~ %r{<(.+?)>} |
|
1218 | 1219 | email = $1 |
|
1219 | 1220 | end |
|
1220 | 1221 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
|
1221 | 1222 | else |
|
1222 | 1223 | '' |
|
1223 | 1224 | end |
|
1224 | 1225 | end |
|
1225 | 1226 | |
|
1226 | 1227 | def sanitize_anchor_name(anchor) |
|
1227 | 1228 | if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' |
|
1228 | 1229 | anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1229 | 1230 | else |
|
1230 | 1231 | # TODO: remove when ruby1.8 is no longer supported |
|
1231 | 1232 | anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1232 | 1233 | end |
|
1233 | 1234 | end |
|
1234 | 1235 | |
|
1235 | 1236 | # Returns the javascript tags that are included in the html layout head |
|
1236 | 1237 | def javascript_heads |
|
1237 | 1238 | tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') |
|
1238 | 1239 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
|
1239 | 1240 | tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") |
|
1240 | 1241 | end |
|
1241 | 1242 | tags |
|
1242 | 1243 | end |
|
1243 | 1244 | |
|
1244 | 1245 | def favicon |
|
1245 | 1246 | "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe |
|
1246 | 1247 | end |
|
1247 | 1248 | |
|
1248 | 1249 | def robot_exclusion_tag |
|
1249 | 1250 | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe |
|
1250 | 1251 | end |
|
1251 | 1252 | |
|
1252 | 1253 | # Returns true if arg is expected in the API response |
|
1253 | 1254 | def include_in_api_response?(arg) |
|
1254 | 1255 | unless @included_in_api_response |
|
1255 | 1256 | param = params[:include] |
|
1256 | 1257 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') |
|
1257 | 1258 | @included_in_api_response.collect!(&:strip) |
|
1258 | 1259 | end |
|
1259 | 1260 | @included_in_api_response.include?(arg.to_s) |
|
1260 | 1261 | end |
|
1261 | 1262 | |
|
1262 | 1263 | # Returns options or nil if nometa param or X-Redmine-Nometa header |
|
1263 | 1264 | # was set in the request |
|
1264 | 1265 | def api_meta(options) |
|
1265 | 1266 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
|
1266 | 1267 | # compatibility mode for activeresource clients that raise |
|
1267 | 1268 | # an error when unserializing an array with attributes |
|
1268 | 1269 | nil |
|
1269 | 1270 | else |
|
1270 | 1271 | options |
|
1271 | 1272 | end |
|
1272 | 1273 | end |
|
1273 | 1274 | |
|
1274 | 1275 | private |
|
1275 | 1276 | |
|
1276 | 1277 | def wiki_helper |
|
1277 | 1278 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
1278 | 1279 | extend helper |
|
1279 | 1280 | return self |
|
1280 | 1281 | end |
|
1281 | 1282 | |
|
1282 | 1283 | def link_to_content_update(text, url_params = {}, html_options = {}) |
|
1283 | 1284 | link_to(text, url_params, html_options) |
|
1284 | 1285 | end |
|
1285 | 1286 | end |
@@ -1,287 +1,295 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require "digest/md5" |
|
19 | 19 | |
|
20 | 20 | class Attachment < ActiveRecord::Base |
|
21 | 21 | belongs_to :container, :polymorphic => true |
|
22 | 22 | belongs_to :author, :class_name => "User", :foreign_key => "author_id" |
|
23 | 23 | |
|
24 | 24 | validates_presence_of :filename, :author |
|
25 | 25 | validates_length_of :filename, :maximum => 255 |
|
26 | 26 | validates_length_of :disk_filename, :maximum => 255 |
|
27 | 27 | validates_length_of :description, :maximum => 255 |
|
28 | 28 | validate :validate_max_file_size |
|
29 | 29 | |
|
30 | 30 | acts_as_event :title => :filename, |
|
31 | 31 | :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}} |
|
32 | 32 | |
|
33 | 33 | acts_as_activity_provider :type => 'files', |
|
34 | 34 | :permission => :view_files, |
|
35 | 35 | :author_key => :author_id, |
|
36 | 36 | :find_options => {:select => "#{Attachment.table_name}.*", |
|
37 | 37 | :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " + |
|
38 | 38 | "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"} |
|
39 | 39 | |
|
40 | 40 | acts_as_activity_provider :type => 'documents', |
|
41 | 41 | :permission => :view_documents, |
|
42 | 42 | :author_key => :author_id, |
|
43 | 43 | :find_options => {:select => "#{Attachment.table_name}.*", |
|
44 | 44 | :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " + |
|
45 | 45 | "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"} |
|
46 | 46 | |
|
47 | 47 | cattr_accessor :storage_path |
|
48 | 48 | @@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files") |
|
49 | 49 | |
|
50 | 50 | cattr_accessor :thumbnails_storage_path |
|
51 | 51 | @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails") |
|
52 | 52 | |
|
53 | 53 | before_save :files_to_final_location |
|
54 | 54 | after_destroy :delete_from_disk |
|
55 | 55 | |
|
56 | 56 | # Returns an unsaved copy of the attachment |
|
57 | 57 | def copy(attributes=nil) |
|
58 | 58 | copy = self.class.new |
|
59 | 59 | copy.attributes = self.attributes.dup.except("id", "downloads") |
|
60 | 60 | copy.attributes = attributes if attributes |
|
61 | 61 | copy |
|
62 | 62 | end |
|
63 | 63 | |
|
64 | 64 | def validate_max_file_size |
|
65 | 65 | if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes |
|
66 | 66 | errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes)) |
|
67 | 67 | end |
|
68 | 68 | end |
|
69 | 69 | |
|
70 | 70 | def file=(incoming_file) |
|
71 | 71 | unless incoming_file.nil? |
|
72 | 72 | @temp_file = incoming_file |
|
73 | 73 | if @temp_file.size > 0 |
|
74 | 74 | if @temp_file.respond_to?(:original_filename) |
|
75 | 75 | self.filename = @temp_file.original_filename |
|
76 | 76 | self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding) |
|
77 | 77 | end |
|
78 | 78 | if @temp_file.respond_to?(:content_type) |
|
79 | 79 | self.content_type = @temp_file.content_type.to_s.chomp |
|
80 | 80 | end |
|
81 | 81 | if content_type.blank? && filename.present? |
|
82 | 82 | self.content_type = Redmine::MimeType.of(filename) |
|
83 | 83 | end |
|
84 | 84 | self.filesize = @temp_file.size |
|
85 | 85 | end |
|
86 | 86 | end |
|
87 | 87 | end |
|
88 | 88 | |
|
89 | 89 | def file |
|
90 | 90 | nil |
|
91 | 91 | end |
|
92 | 92 | |
|
93 | 93 | def filename=(arg) |
|
94 | 94 | write_attribute :filename, sanitize_filename(arg.to_s) |
|
95 | 95 | if new_record? && disk_filename.blank? |
|
96 | 96 | self.disk_filename = Attachment.disk_filename(filename) |
|
97 | 97 | end |
|
98 | 98 | filename |
|
99 | 99 | end |
|
100 | 100 | |
|
101 | 101 | # Copies the temporary file to its final location |
|
102 | 102 | # and computes its MD5 hash |
|
103 | 103 | def files_to_final_location |
|
104 | 104 | if @temp_file && (@temp_file.size > 0) |
|
105 | 105 | logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)") |
|
106 | 106 | md5 = Digest::MD5.new |
|
107 | 107 | File.open(diskfile, "wb") do |f| |
|
108 | 108 | if @temp_file.respond_to?(:read) |
|
109 | 109 | buffer = "" |
|
110 | 110 | while (buffer = @temp_file.read(8192)) |
|
111 | 111 | f.write(buffer) |
|
112 | 112 | md5.update(buffer) |
|
113 | 113 | end |
|
114 | 114 | else |
|
115 | 115 | f.write(@temp_file) |
|
116 | 116 | md5.update(@temp_file) |
|
117 | 117 | end |
|
118 | 118 | end |
|
119 | 119 | self.digest = md5.hexdigest |
|
120 | 120 | end |
|
121 | 121 | @temp_file = nil |
|
122 | 122 | # Don't save the content type if it's longer than the authorized length |
|
123 | 123 | if self.content_type && self.content_type.length > 255 |
|
124 | 124 | self.content_type = nil |
|
125 | 125 | end |
|
126 | 126 | end |
|
127 | 127 | |
|
128 | 128 | # Deletes the file from the file system if it's not referenced by other attachments |
|
129 | 129 | def delete_from_disk |
|
130 | 130 | if Attachment.where("disk_filename = ? AND id <> ?", disk_filename, id).empty? |
|
131 | 131 | delete_from_disk! |
|
132 | 132 | end |
|
133 | 133 | end |
|
134 | 134 | |
|
135 | 135 | # Returns file's location on disk |
|
136 | 136 | def diskfile |
|
137 | 137 | File.join(self.class.storage_path, disk_filename.to_s) |
|
138 | 138 | end |
|
139 | 139 | |
|
140 | 140 | def title |
|
141 | 141 | title = filename.to_s |
|
142 | 142 | if description.present? |
|
143 | 143 | title << " (#{description})" |
|
144 | 144 | end |
|
145 | 145 | title |
|
146 | 146 | end |
|
147 | 147 | |
|
148 | 148 | def increment_download |
|
149 | 149 | increment!(:downloads) |
|
150 | 150 | end |
|
151 | 151 | |
|
152 | 152 | def project |
|
153 | 153 | container.try(:project) |
|
154 | 154 | end |
|
155 | 155 | |
|
156 | 156 | def visible?(user=User.current) |
|
157 | container && container.attachments_visible?(user) | |
|
157 | if container_id | |
|
158 | container && container.attachments_visible?(user) | |
|
159 | else | |
|
160 | author == user | |
|
161 | end | |
|
158 | 162 | end |
|
159 | 163 | |
|
160 | 164 | def deletable?(user=User.current) |
|
161 | container && container.attachments_deletable?(user) | |
|
165 | if container_id | |
|
166 | container && container.attachments_deletable?(user) | |
|
167 | else | |
|
168 | author == user | |
|
169 | end | |
|
162 | 170 | end |
|
163 | 171 | |
|
164 | 172 | def image? |
|
165 | 173 | !!(self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i) |
|
166 | 174 | end |
|
167 | 175 | |
|
168 | 176 | def thumbnailable? |
|
169 | 177 | image? |
|
170 | 178 | end |
|
171 | 179 | |
|
172 | 180 | # Returns the full path the attachment thumbnail, or nil |
|
173 | 181 | # if the thumbnail cannot be generated. |
|
174 | 182 | def thumbnail(options={}) |
|
175 | 183 | if thumbnailable? && readable? |
|
176 | 184 | size = options[:size].to_i |
|
177 | 185 | if size > 0 |
|
178 | 186 | # Limit the number of thumbnails per image |
|
179 | 187 | size = (size / 50) * 50 |
|
180 | 188 | # Maximum thumbnail size |
|
181 | 189 | size = 800 if size > 800 |
|
182 | 190 | else |
|
183 | 191 | size = Setting.thumbnails_size.to_i |
|
184 | 192 | end |
|
185 | 193 | size = 100 unless size > 0 |
|
186 | 194 | target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb") |
|
187 | 195 | |
|
188 | 196 | begin |
|
189 | 197 | Redmine::Thumbnail.generate(self.diskfile, target, size) |
|
190 | 198 | rescue => e |
|
191 | 199 | logger.error "An error occured while generating thumbnail for #{disk_filename} to #{target}\nException was: #{e.message}" if logger |
|
192 | 200 | return nil |
|
193 | 201 | end |
|
194 | 202 | end |
|
195 | 203 | end |
|
196 | 204 | |
|
197 | 205 | # Deletes all thumbnails |
|
198 | 206 | def self.clear_thumbnails |
|
199 | 207 | Dir.glob(File.join(thumbnails_storage_path, "*.thumb")).each do |file| |
|
200 | 208 | File.delete file |
|
201 | 209 | end |
|
202 | 210 | end |
|
203 | 211 | |
|
204 | 212 | def is_text? |
|
205 | 213 | Redmine::MimeType.is_type?('text', filename) |
|
206 | 214 | end |
|
207 | 215 | |
|
208 | 216 | def is_diff? |
|
209 | 217 | self.filename =~ /\.(patch|diff)$/i |
|
210 | 218 | end |
|
211 | 219 | |
|
212 | 220 | # Returns true if the file is readable |
|
213 | 221 | def readable? |
|
214 | 222 | File.readable?(diskfile) |
|
215 | 223 | end |
|
216 | 224 | |
|
217 | 225 | # Returns the attachment token |
|
218 | 226 | def token |
|
219 | 227 | "#{id}.#{digest}" |
|
220 | 228 | end |
|
221 | 229 | |
|
222 | 230 | # Finds an attachment that matches the given token and that has no container |
|
223 | 231 | def self.find_by_token(token) |
|
224 | 232 | if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/ |
|
225 | 233 | attachment_id, attachment_digest = $1, $2 |
|
226 | 234 | attachment = Attachment.where(:id => attachment_id, :digest => attachment_digest).first |
|
227 | 235 | if attachment && attachment.container.nil? |
|
228 | 236 | attachment |
|
229 | 237 | end |
|
230 | 238 | end |
|
231 | 239 | end |
|
232 | 240 | |
|
233 | 241 | # Bulk attaches a set of files to an object |
|
234 | 242 | # |
|
235 | 243 | # Returns a Hash of the results: |
|
236 | 244 | # :files => array of the attached files |
|
237 | 245 | # :unsaved => array of the files that could not be attached |
|
238 | 246 | def self.attach_files(obj, attachments) |
|
239 | 247 | result = obj.save_attachments(attachments, User.current) |
|
240 | 248 | obj.attach_saved_attachments |
|
241 | 249 | result |
|
242 | 250 | end |
|
243 | 251 | |
|
244 | 252 | def self.latest_attach(attachments, filename) |
|
245 | 253 | attachments.sort_by(&:created_on).reverse.detect { |
|
246 | 254 | |att| att.filename.downcase == filename.downcase |
|
247 | 255 | } |
|
248 | 256 | end |
|
249 | 257 | |
|
250 | 258 | def self.prune(age=1.day) |
|
251 | 259 | Attachment.where("created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age).destroy_all |
|
252 | 260 | end |
|
253 | 261 | |
|
254 | 262 | private |
|
255 | 263 | |
|
256 | 264 | # Physically deletes the file from the file system |
|
257 | 265 | def delete_from_disk! |
|
258 | 266 | if disk_filename.present? && File.exist?(diskfile) |
|
259 | 267 | File.delete(diskfile) |
|
260 | 268 | end |
|
261 | 269 | end |
|
262 | 270 | |
|
263 | 271 | def sanitize_filename(value) |
|
264 | 272 | # get only the filename, not the whole path |
|
265 | 273 | just_filename = value.gsub(/^.*(\\|\/)/, '') |
|
266 | 274 | |
|
267 | 275 | # Finally, replace invalid characters with underscore |
|
268 | 276 | @filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_') |
|
269 | 277 | end |
|
270 | 278 | |
|
271 | 279 | # Returns an ASCII or hashed filename |
|
272 | 280 | def self.disk_filename(filename) |
|
273 | 281 | timestamp = DateTime.now.strftime("%y%m%d%H%M%S") |
|
274 | 282 | ascii = '' |
|
275 | 283 | if filename =~ %r{^[a-zA-Z0-9_\.\-]*$} |
|
276 | 284 | ascii = filename |
|
277 | 285 | else |
|
278 | 286 | ascii = Digest::MD5.hexdigest(filename) |
|
279 | 287 | # keep the extension if any |
|
280 | 288 | ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$} |
|
281 | 289 | end |
|
282 | 290 | while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}")) |
|
283 | 291 | timestamp.succ! |
|
284 | 292 | end |
|
285 | 293 | "#{timestamp}_#{ascii}" |
|
286 | 294 | end |
|
287 | 295 | end |
@@ -1,18 +1,28 | |||
|
1 | <span id="attachments_fields"> | |
|
1 | 2 | <% if defined?(container) && container && container.saved_attachments %> |
|
2 | 3 | <% container.saved_attachments.each_with_index do |attachment, i| %> |
|
3 | <span class="icon icon-attachment" style="display:block; line-height:1.5em;"> | |
|
4 | <%= h(attachment.filename) %> (<%= number_to_human_size(attachment.filesize) %>) | |
|
5 | <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.id}.#{attachment.digest}" %> | |
|
4 | <span id="attachments_p<%= i %>"> | |
|
5 | <%= text_field_tag("attachments[p#{i}][filename]", attachment.filename, :class => 'filename') + | |
|
6 | text_field_tag("attachments[p#{i}][description]", attachment.description, :maxlength => 255, :placeholder => l(:label_optional_description), :class => 'description') + | |
|
7 | link_to(' '.html_safe, attachment_path(attachment, :attachment_id => "p#{i}", :format => 'js'), :method => 'delete', :remote => true, :class => 'remove-upload') %> | |
|
8 | <%= hidden_field_tag "attachments[p#{i}][token]", "#{attachment.token}" %> | |
|
6 | 9 | </span> |
|
7 | 10 | <% end %> |
|
8 | 11 | <% end %> |
|
9 | <span id="attachments_fields"> | |
|
10 | <span> | |
|
11 | <%= file_field_tag 'attachments[1][file]', :id => nil, :class => 'file', | |
|
12 | :onchange => "checkFileSize(this, #{Setting.attachment_max_size.to_i.kilobytes}, '#{escape_javascript(l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)))}');" -%> | |
|
13 | <%= text_field_tag 'attachments[1][description]', '', :id => nil, :class => 'description', :maxlength => 255, :placeholder => l(:label_optional_description) %> | |
|
14 | <%= link_to_function(image_tag('delete.png'), 'removeFileField(this)', :title => (l(:button_delete))) %> | |
|
15 | </span> | |
|
16 | 12 | </span> |
|
17 | <span class="add_attachment"><%= link_to l(:label_add_another_file), '#', :onclick => 'addFileField(); return false;', :class => 'add_attachment' %> | |
|
18 | (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>)</span> | |
|
13 | <span class="add_attachment"> | |
|
14 | <%= file_field_tag 'attachments_files', | |
|
15 | :id => nil, | |
|
16 | :multiple => true, | |
|
17 | :onchange => 'addInputFiles(this);', | |
|
18 | :data => { | |
|
19 | :max_file_size => Setting.attachment_max_size.to_i.kilobytes, | |
|
20 | :max_file_size_message => l(:error_attachment_too_big, :max_size => number_to_human_size(Setting.attachment_max_size.to_i.kilobytes)), | |
|
21 | :max_concurrent_uploads => Redmine::Configuration['max_concurrent_ajax_uploads'].to_i, | |
|
22 | :upload_path => uploads_path(:format => 'js'), | |
|
23 | :description_placeholder => l(:label_optional_description) | |
|
24 | } %> | |
|
25 | (<%= l(:label_max_size) %>: <%= number_to_human_size(Setting.attachment_max_size.to_i.kilobytes) %>) | |
|
26 | </span> | |
|
27 | ||
|
28 | <%= javascript_include_tag 'attachments' %> |
@@ -1,3 +1,3 | |||
|
1 | 1 | <fieldset class="preview"><legend><%= l(:label_preview) %></legend> |
|
2 |
<%= textilizable @text, :attachments => @attach |
|
|
2 | <%= textilizable @text, :attachments => @attachments, :object => @previewed %> | |
|
3 | 3 | </fieldset> |
@@ -1,11 +1,11 | |||
|
1 | 1 | <% if @notes %> |
|
2 | 2 | <fieldset class="preview"><legend><%= l(:field_notes) %></legend> |
|
3 |
<%= textilizable @notes, :attachments => @attach |
|
|
3 | <%= textilizable @notes, :attachments => @attachments, :object => @issue %> | |
|
4 | 4 | </fieldset> |
|
5 | 5 | <% end %> |
|
6 | 6 | |
|
7 | 7 | <% if @description %> |
|
8 | 8 | <fieldset class="preview"><legend><%= l(:field_description) %></legend> |
|
9 |
<%= textilizable @description, :attachments => @attach |
|
|
9 | <%= textilizable @description, :attachments => @attachments, :object => @issue %> | |
|
10 | 10 | </fieldset> |
|
11 | 11 | <% end %> |
@@ -1,197 +1,200 | |||
|
1 | 1 | # = Redmine configuration file |
|
2 | 2 | # |
|
3 | 3 | # Each environment has it's own configuration options. If you are only |
|
4 | 4 | # running in production, only the production block needs to be configured. |
|
5 | 5 | # Environment specific configuration options override the default ones. |
|
6 | 6 | # |
|
7 | 7 | # Note that this file needs to be a valid YAML file. |
|
8 | 8 | # DO NOT USE TABS! Use 2 spaces instead of tabs for identation. |
|
9 | 9 | # |
|
10 | 10 | # == Outgoing email settings (email_delivery setting) |
|
11 | 11 | # |
|
12 | 12 | # === Common configurations |
|
13 | 13 | # |
|
14 | 14 | # ==== Sendmail command |
|
15 | 15 | # |
|
16 | 16 | # production: |
|
17 | 17 | # email_delivery: |
|
18 | 18 | # delivery_method: :sendmail |
|
19 | 19 | # |
|
20 | 20 | # ==== Simple SMTP server at localhost |
|
21 | 21 | # |
|
22 | 22 | # production: |
|
23 | 23 | # email_delivery: |
|
24 | 24 | # delivery_method: :smtp |
|
25 | 25 | # smtp_settings: |
|
26 | 26 | # address: "localhost" |
|
27 | 27 | # port: 25 |
|
28 | 28 | # |
|
29 | 29 | # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com |
|
30 | 30 | # |
|
31 | 31 | # production: |
|
32 | 32 | # email_delivery: |
|
33 | 33 | # delivery_method: :smtp |
|
34 | 34 | # smtp_settings: |
|
35 | 35 | # address: "example.com" |
|
36 | 36 | # port: 25 |
|
37 | 37 | # authentication: :login |
|
38 | 38 | # domain: 'foo.com' |
|
39 | 39 | # user_name: 'myaccount' |
|
40 | 40 | # password: 'password' |
|
41 | 41 | # |
|
42 | 42 | # ==== SMTP server at example.com using PLAIN authentication |
|
43 | 43 | # |
|
44 | 44 | # production: |
|
45 | 45 | # email_delivery: |
|
46 | 46 | # delivery_method: :smtp |
|
47 | 47 | # smtp_settings: |
|
48 | 48 | # address: "example.com" |
|
49 | 49 | # port: 25 |
|
50 | 50 | # authentication: :plain |
|
51 | 51 | # domain: 'example.com' |
|
52 | 52 | # user_name: 'myaccount' |
|
53 | 53 | # password: 'password' |
|
54 | 54 | # |
|
55 | 55 | # ==== SMTP server at using TLS (GMail) |
|
56 | 56 | # |
|
57 | 57 | # This might require some additional configuration. See the guides at: |
|
58 | 58 | # http://www.redmine.org/projects/redmine/wiki/EmailConfiguration |
|
59 | 59 | # |
|
60 | 60 | # production: |
|
61 | 61 | # email_delivery: |
|
62 | 62 | # delivery_method: :smtp |
|
63 | 63 | # smtp_settings: |
|
64 | 64 | # enable_starttls_auto: true |
|
65 | 65 | # address: "smtp.gmail.com" |
|
66 | 66 | # port: 587 |
|
67 | 67 | # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps |
|
68 | 68 | # authentication: :plain |
|
69 | 69 | # user_name: "your_email@gmail.com" |
|
70 | 70 | # password: "your_password" |
|
71 | 71 | # |
|
72 | 72 | # |
|
73 | 73 | # === More configuration options |
|
74 | 74 | # |
|
75 | 75 | # See the "Configuration options" at the following website for a list of the |
|
76 | 76 | # full options allowed: |
|
77 | 77 | # |
|
78 | 78 | # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer |
|
79 | 79 | |
|
80 | 80 | |
|
81 | 81 | # default configuration options for all environments |
|
82 | 82 | default: |
|
83 | 83 | # Outgoing emails configuration (see examples above) |
|
84 | 84 | email_delivery: |
|
85 | 85 | delivery_method: :smtp |
|
86 | 86 | smtp_settings: |
|
87 | 87 | address: smtp.example.net |
|
88 | 88 | port: 25 |
|
89 | 89 | domain: example.net |
|
90 | 90 | authentication: :login |
|
91 | 91 | user_name: "redmine@example.net" |
|
92 | 92 | password: "redmine" |
|
93 | 93 | |
|
94 | 94 | # Absolute path to the directory where attachments are stored. |
|
95 | 95 | # The default is the 'files' directory in your Redmine instance. |
|
96 | 96 | # Your Redmine instance needs to have write permission on this |
|
97 | 97 | # directory. |
|
98 | 98 | # Examples: |
|
99 | 99 | # attachments_storage_path: /var/redmine/files |
|
100 | 100 | # attachments_storage_path: D:/redmine/files |
|
101 | 101 | attachments_storage_path: |
|
102 | 102 | |
|
103 | 103 | # Configuration of the autologin cookie. |
|
104 | 104 | # autologin_cookie_name: the name of the cookie (default: autologin) |
|
105 | 105 | # autologin_cookie_path: the cookie path (default: /) |
|
106 | 106 | # autologin_cookie_secure: true sets the cookie secure flag (default: false) |
|
107 | 107 | autologin_cookie_name: |
|
108 | 108 | autologin_cookie_path: |
|
109 | 109 | autologin_cookie_secure: |
|
110 | 110 | |
|
111 | 111 | # Configuration of SCM executable command. |
|
112 | 112 | # |
|
113 | 113 | # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe) |
|
114 | 114 | # On Windows + CRuby, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work. |
|
115 | 115 | # |
|
116 | 116 | # On Windows + JRuby 1.6.2, path which contains spaces does not work. |
|
117 | 117 | # For example, "C:\Program Files\TortoiseHg\hg.exe". |
|
118 | 118 | # If you want to this feature, you need to install to the path which does not contains spaces. |
|
119 | 119 | # For example, "C:\TortoiseHg\hg.exe". |
|
120 | 120 | # |
|
121 | 121 | # Examples: |
|
122 | 122 | # scm_subversion_command: svn # (default: svn) |
|
123 | 123 | # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg) |
|
124 | 124 | # scm_git_command: /usr/local/bin/git # (default: git) |
|
125 | 125 | # scm_cvs_command: cvs # (default: cvs) |
|
126 | 126 | # scm_bazaar_command: bzr.exe # (default: bzr) |
|
127 | 127 | # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs) |
|
128 | 128 | # |
|
129 | 129 | scm_subversion_command: |
|
130 | 130 | scm_mercurial_command: |
|
131 | 131 | scm_git_command: |
|
132 | 132 | scm_cvs_command: |
|
133 | 133 | scm_bazaar_command: |
|
134 | 134 | scm_darcs_command: |
|
135 | 135 | |
|
136 | 136 | # Key used to encrypt sensitive data in the database (SCM and LDAP passwords). |
|
137 | 137 | # If you don't want to enable data encryption, just leave it blank. |
|
138 | 138 | # WARNING: losing/changing this key will make encrypted data unreadable. |
|
139 | 139 | # |
|
140 | 140 | # If you want to encrypt existing passwords in your database: |
|
141 | 141 | # * set the cipher key here in your configuration file |
|
142 | 142 | # * encrypt data using 'rake db:encrypt RAILS_ENV=production' |
|
143 | 143 | # |
|
144 | 144 | # If you have encrypted data and want to change this key, you have to: |
|
145 | 145 | # * decrypt data using 'rake db:decrypt RAILS_ENV=production' first |
|
146 | 146 | # * change the cipher key here in your configuration file |
|
147 | 147 | # * encrypt data using 'rake db:encrypt RAILS_ENV=production' |
|
148 | 148 | database_cipher_key: |
|
149 | 149 | |
|
150 | 150 | # Set this to false to disable plugins' assets mirroring on startup. |
|
151 | 151 | # You can use `rake redmine:plugins:assets` to manually mirror assets |
|
152 | 152 | # to public/plugin_assets when you install/upgrade a Redmine plugin. |
|
153 | 153 | # |
|
154 | 154 | #mirror_plugins_assets_on_startup: false |
|
155 | 155 | |
|
156 | 156 | # Your secret key for verifying cookie session data integrity. If you |
|
157 | 157 | # change this key, all old sessions will become invalid! Make sure the |
|
158 | 158 | # secret is at least 30 characters and all random, no regular words or |
|
159 | 159 | # you'll be exposed to dictionary attacks. |
|
160 | 160 | # |
|
161 | 161 | # If you have a load-balancing Redmine cluster, you have to use the |
|
162 | 162 | # same secret token on each machine. |
|
163 | 163 | #secret_token: 'change it to a long random string' |
|
164 | 164 | |
|
165 | 165 | # Absolute path (e.g. /usr/bin/convert, c:/im/convert.exe) to |
|
166 | 166 | # the ImageMagick's `convert` binary. Used to generate attachment thumbnails. |
|
167 | 167 | #imagemagick_convert_command: |
|
168 | 168 | |
|
169 | 169 | # Configuration of RMagcik font. |
|
170 | 170 | # |
|
171 | 171 | # Redmine uses RMagcik in order to export gantt png. |
|
172 | 172 | # You don't need this setting if you don't install RMagcik. |
|
173 | 173 | # |
|
174 | 174 | # In CJK (Chinese, Japanese and Korean), |
|
175 | 175 | # in order to show CJK characters correctly, |
|
176 | 176 | # you need to set this configuration. |
|
177 | 177 | # |
|
178 | 178 | # Because there is no standard font across platforms in CJK, |
|
179 | 179 | # you need to set a font installed in your server. |
|
180 | 180 | # |
|
181 | 181 | # This setting is not necessary in non CJK. |
|
182 | 182 | # |
|
183 | 183 | # Examples for Japanese: |
|
184 | 184 | # Windows: |
|
185 | 185 | # rmagick_font_path: C:\windows\fonts\msgothic.ttc |
|
186 | 186 | # Linux: |
|
187 | 187 | # rmagick_font_path: /usr/share/fonts/ipa-mincho/ipam.ttf |
|
188 | 188 | # |
|
189 | 189 | rmagick_font_path: |
|
190 | 190 | |
|
191 | # Maximum number of simultaneous AJAX uploads | |
|
192 | #max_concurrent_ajax_uploads: 2 | |
|
193 | ||
|
191 | 194 | # specific configuration options for production environment |
|
192 | 195 | # that overrides the default ones |
|
193 | 196 | production: |
|
194 | 197 | |
|
195 | 198 | # specific configuration options for development environment |
|
196 | 199 | # that overrides the default ones |
|
197 | 200 | development: |
@@ -1,113 +1,114 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | module Redmine |
|
19 | 19 | module Configuration |
|
20 | 20 | |
|
21 | 21 | # Configuration default values |
|
22 | 22 | @defaults = { |
|
23 | 'email_delivery' => nil | |
|
23 | 'email_delivery' => nil, | |
|
24 | 'max_concurrent_ajax_uploads' => 2 | |
|
24 | 25 | } |
|
25 | 26 | |
|
26 | 27 | @config = nil |
|
27 | 28 | |
|
28 | 29 | class << self |
|
29 | 30 | # Loads the Redmine configuration file |
|
30 | 31 | # Valid options: |
|
31 | 32 | # * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml) |
|
32 | 33 | # * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env) |
|
33 | 34 | def load(options={}) |
|
34 | 35 | filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml') |
|
35 | 36 | env = options[:env] || Rails.env |
|
36 | 37 | |
|
37 | 38 | @config = @defaults.dup |
|
38 | 39 | |
|
39 | 40 | load_deprecated_email_configuration(env) |
|
40 | 41 | if File.file?(filename) |
|
41 | 42 | @config.merge!(load_from_yaml(filename, env)) |
|
42 | 43 | end |
|
43 | 44 | |
|
44 | 45 | # Compatibility mode for those who copy email.yml over configuration.yml |
|
45 | 46 | %w(delivery_method smtp_settings sendmail_settings).each do |key| |
|
46 | 47 | if value = @config.delete(key) |
|
47 | 48 | @config['email_delivery'] ||= {} |
|
48 | 49 | @config['email_delivery'][key] = value |
|
49 | 50 | end |
|
50 | 51 | end |
|
51 | 52 | |
|
52 | 53 | if @config['email_delivery'] |
|
53 | 54 | ActionMailer::Base.perform_deliveries = true |
|
54 | 55 | @config['email_delivery'].each do |k, v| |
|
55 | 56 | v.symbolize_keys! if v.respond_to?(:symbolize_keys!) |
|
56 | 57 | ActionMailer::Base.send("#{k}=", v) |
|
57 | 58 | end |
|
58 | 59 | end |
|
59 | 60 | |
|
60 | 61 | @config |
|
61 | 62 | end |
|
62 | 63 | |
|
63 | 64 | # Returns a configuration setting |
|
64 | 65 | def [](name) |
|
65 | 66 | load unless @config |
|
66 | 67 | @config[name] |
|
67 | 68 | end |
|
68 | 69 | |
|
69 | 70 | # Yields a block with the specified hash configuration settings |
|
70 | 71 | def with(settings) |
|
71 | 72 | settings.stringify_keys! |
|
72 | 73 | load unless @config |
|
73 | 74 | was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h} |
|
74 | 75 | @config.merge! settings |
|
75 | 76 | yield if block_given? |
|
76 | 77 | @config.merge! was |
|
77 | 78 | end |
|
78 | 79 | |
|
79 | 80 | private |
|
80 | 81 | |
|
81 | 82 | def load_from_yaml(filename, env) |
|
82 | 83 | yaml = nil |
|
83 | 84 | begin |
|
84 | 85 | yaml = YAML::load_file(filename) |
|
85 | 86 | rescue ArgumentError |
|
86 | 87 | $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded." |
|
87 | 88 | exit 1 |
|
88 | 89 | end |
|
89 | 90 | conf = {} |
|
90 | 91 | if yaml.is_a?(Hash) |
|
91 | 92 | if yaml['default'] |
|
92 | 93 | conf.merge!(yaml['default']) |
|
93 | 94 | end |
|
94 | 95 | if yaml[env] |
|
95 | 96 | conf.merge!(yaml[env]) |
|
96 | 97 | end |
|
97 | 98 | else |
|
98 | 99 | $stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file." |
|
99 | 100 | exit 1 |
|
100 | 101 | end |
|
101 | 102 | conf |
|
102 | 103 | end |
|
103 | 104 | |
|
104 | 105 | def load_deprecated_email_configuration(env) |
|
105 | 106 | deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml') |
|
106 | 107 | if File.file?(deprecated_email_conf) |
|
107 | 108 | warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting." |
|
108 | 109 | @config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)}) |
|
109 | 110 | end |
|
110 | 111 | end |
|
111 | 112 | end |
|
112 | 113 | end |
|
113 | 114 | end |
@@ -1,611 +1,582 | |||
|
1 | 1 | /* Redmine - project management software |
|
2 | 2 | Copyright (C) 2006-2012 Jean-Philippe Lang */ |
|
3 | 3 | |
|
4 | 4 | function checkAll(id, checked) { |
|
5 | 5 | if (checked) { |
|
6 | 6 | $('#'+id).find('input[type=checkbox]').attr('checked', true); |
|
7 | 7 | } else { |
|
8 | 8 | $('#'+id).find('input[type=checkbox]').removeAttr('checked'); |
|
9 | 9 | } |
|
10 | 10 | } |
|
11 | 11 | |
|
12 | 12 | function toggleCheckboxesBySelector(selector) { |
|
13 | 13 | var all_checked = true; |
|
14 | 14 | $(selector).each(function(index) { |
|
15 | 15 | if (!$(this).is(':checked')) { all_checked = false; } |
|
16 | 16 | }); |
|
17 | 17 | $(selector).attr('checked', !all_checked) |
|
18 | 18 | } |
|
19 | 19 | |
|
20 | 20 | function showAndScrollTo(id, focus) { |
|
21 | 21 | $('#'+id).show(); |
|
22 | 22 | if (focus!=null) { |
|
23 | 23 | $('#'+focus).focus(); |
|
24 | 24 | } |
|
25 | 25 | $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100); |
|
26 | 26 | } |
|
27 | 27 | |
|
28 | 28 | function toggleRowGroup(el) { |
|
29 | 29 | var tr = $(el).parents('tr').first(); |
|
30 | 30 | var n = tr.next(); |
|
31 | 31 | tr.toggleClass('open'); |
|
32 | 32 | while (n.length && !n.hasClass('group')) { |
|
33 | 33 | n.toggle(); |
|
34 | 34 | n = n.next('tr'); |
|
35 | 35 | } |
|
36 | 36 | } |
|
37 | 37 | |
|
38 | 38 | function collapseAllRowGroups(el) { |
|
39 | 39 | var tbody = $(el).parents('tbody').first(); |
|
40 | 40 | tbody.children('tr').each(function(index) { |
|
41 | 41 | if ($(this).hasClass('group')) { |
|
42 | 42 | $(this).removeClass('open'); |
|
43 | 43 | } else { |
|
44 | 44 | $(this).hide(); |
|
45 | 45 | } |
|
46 | 46 | }); |
|
47 | 47 | } |
|
48 | 48 | |
|
49 | 49 | function expandAllRowGroups(el) { |
|
50 | 50 | var tbody = $(el).parents('tbody').first(); |
|
51 | 51 | tbody.children('tr').each(function(index) { |
|
52 | 52 | if ($(this).hasClass('group')) { |
|
53 | 53 | $(this).addClass('open'); |
|
54 | 54 | } else { |
|
55 | 55 | $(this).show(); |
|
56 | 56 | } |
|
57 | 57 | }); |
|
58 | 58 | } |
|
59 | 59 | |
|
60 | 60 | function toggleAllRowGroups(el) { |
|
61 | 61 | var tr = $(el).parents('tr').first(); |
|
62 | 62 | if (tr.hasClass('open')) { |
|
63 | 63 | collapseAllRowGroups(el); |
|
64 | 64 | } else { |
|
65 | 65 | expandAllRowGroups(el); |
|
66 | 66 | } |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | function toggleFieldset(el) { |
|
70 | 70 | var fieldset = $(el).parents('fieldset').first(); |
|
71 | 71 | fieldset.toggleClass('collapsed'); |
|
72 | 72 | fieldset.children('div').toggle(); |
|
73 | 73 | } |
|
74 | 74 | |
|
75 | 75 | function hideFieldset(el) { |
|
76 | 76 | var fieldset = $(el).parents('fieldset').first(); |
|
77 | 77 | fieldset.toggleClass('collapsed'); |
|
78 | 78 | fieldset.children('div').hide(); |
|
79 | 79 | } |
|
80 | 80 | |
|
81 | 81 | function initFilters(){ |
|
82 | 82 | $('#add_filter_select').change(function(){ |
|
83 | 83 | addFilter($(this).val(), '', []); |
|
84 | 84 | }); |
|
85 | 85 | $('#filters-table td.field input[type=checkbox]').each(function(){ |
|
86 | 86 | toggleFilter($(this).val()); |
|
87 | 87 | }); |
|
88 | 88 | $('#filters-table td.field input[type=checkbox]').live('click',function(){ |
|
89 | 89 | toggleFilter($(this).val()); |
|
90 | 90 | }); |
|
91 | 91 | $('#filters-table .toggle-multiselect').live('click',function(){ |
|
92 | 92 | toggleMultiSelect($(this).siblings('select')); |
|
93 | 93 | }); |
|
94 | 94 | $('#filters-table input[type=text]').live('keypress', function(e){ |
|
95 | 95 | if (e.keyCode == 13) submit_query_form("query_form"); |
|
96 | 96 | }); |
|
97 | 97 | } |
|
98 | 98 | |
|
99 | 99 | function addFilter(field, operator, values) { |
|
100 | 100 | var fieldId = field.replace('.', '_'); |
|
101 | 101 | var tr = $('#tr_'+fieldId); |
|
102 | 102 | if (tr.length > 0) { |
|
103 | 103 | tr.show(); |
|
104 | 104 | } else { |
|
105 | 105 | buildFilterRow(field, operator, values); |
|
106 | 106 | } |
|
107 | 107 | $('#cb_'+fieldId).attr('checked', true); |
|
108 | 108 | toggleFilter(field); |
|
109 | 109 | $('#add_filter_select').val('').children('option').each(function(){ |
|
110 | 110 | if ($(this).attr('value') == field) { |
|
111 | 111 | $(this).attr('disabled', true); |
|
112 | 112 | } |
|
113 | 113 | }); |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | function buildFilterRow(field, operator, values) { |
|
117 | 117 | var fieldId = field.replace('.', '_'); |
|
118 | 118 | var filterTable = $("#filters-table"); |
|
119 | 119 | var filterOptions = availableFilters[field]; |
|
120 | 120 | var operators = operatorByType[filterOptions['type']]; |
|
121 | 121 | var filterValues = filterOptions['values']; |
|
122 | 122 | var i, select; |
|
123 | 123 | |
|
124 | 124 | var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html( |
|
125 | 125 | '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' + |
|
126 | 126 | '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' + |
|
127 | 127 | '<td class="values"></td>' |
|
128 | 128 | ); |
|
129 | 129 | filterTable.append(tr); |
|
130 | 130 | |
|
131 | 131 | select = tr.find('td.operator select'); |
|
132 | 132 | for (i=0;i<operators.length;i++){ |
|
133 | 133 | var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]); |
|
134 | 134 | if (operators[i] == operator) {option.attr('selected', true)}; |
|
135 | 135 | select.append(option); |
|
136 | 136 | } |
|
137 | 137 | select.change(function(){toggleOperator(field)}); |
|
138 | 138 | |
|
139 | 139 | switch (filterOptions['type']){ |
|
140 | 140 | case "list": |
|
141 | 141 | case "list_optional": |
|
142 | 142 | case "list_status": |
|
143 | 143 | case "list_subprojects": |
|
144 | 144 | tr.find('td.values').append( |
|
145 | 145 | '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' + |
|
146 | 146 | ' <span class="toggle-multiselect"> </span></span>' |
|
147 | 147 | ); |
|
148 | 148 | select = tr.find('td.values select'); |
|
149 | 149 | if (values.length > 1) {select.attr('multiple', true)}; |
|
150 | 150 | for (i=0;i<filterValues.length;i++){ |
|
151 | 151 | var filterValue = filterValues[i]; |
|
152 | 152 | var option = $('<option>'); |
|
153 | 153 | if ($.isArray(filterValue)) { |
|
154 | 154 | option.val(filterValue[1]).text(filterValue[0]); |
|
155 | 155 | if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);} |
|
156 | 156 | } else { |
|
157 | 157 | option.val(filterValue).text(filterValue); |
|
158 | 158 | if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);} |
|
159 | 159 | } |
|
160 | 160 | select.append(option); |
|
161 | 161 | } |
|
162 | 162 | break; |
|
163 | 163 | case "date": |
|
164 | 164 | case "date_past": |
|
165 | 165 | tr.find('td.values').append( |
|
166 | 166 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' + |
|
167 | 167 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' + |
|
168 | 168 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>' |
|
169 | 169 | ); |
|
170 | 170 | $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions); |
|
171 | 171 | $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions); |
|
172 | 172 | $('#values_'+fieldId).val(values[0]); |
|
173 | 173 | break; |
|
174 | 174 | case "string": |
|
175 | 175 | case "text": |
|
176 | 176 | tr.find('td.values').append( |
|
177 | 177 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>' |
|
178 | 178 | ); |
|
179 | 179 | $('#values_'+fieldId).val(values[0]); |
|
180 | 180 | break; |
|
181 | 181 | case "relation": |
|
182 | 182 | tr.find('td.values').append( |
|
183 | 183 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' + |
|
184 | 184 | '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>' |
|
185 | 185 | ); |
|
186 | 186 | $('#values_'+fieldId).val(values[0]); |
|
187 | 187 | select = tr.find('td.values select'); |
|
188 | 188 | for (i=0;i<allProjects.length;i++){ |
|
189 | 189 | var filterValue = allProjects[i]; |
|
190 | 190 | var option = $('<option>'); |
|
191 | 191 | option.val(filterValue[1]).text(filterValue[0]); |
|
192 | 192 | if (values[0] == filterValue[1]) {option.attr('selected', true)}; |
|
193 | 193 | select.append(option); |
|
194 | 194 | } |
|
195 | 195 | case "integer": |
|
196 | 196 | case "float": |
|
197 | 197 | tr.find('td.values').append( |
|
198 | 198 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' + |
|
199 | 199 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>' |
|
200 | 200 | ); |
|
201 | 201 | $('#values_'+fieldId+'_1').val(values[0]); |
|
202 | 202 | $('#values_'+fieldId+'_2').val(values[1]); |
|
203 | 203 | break; |
|
204 | 204 | } |
|
205 | 205 | } |
|
206 | 206 | |
|
207 | 207 | function toggleFilter(field) { |
|
208 | 208 | var fieldId = field.replace('.', '_'); |
|
209 | 209 | if ($('#cb_' + fieldId).is(':checked')) { |
|
210 | 210 | $("#operators_" + fieldId).show().removeAttr('disabled'); |
|
211 | 211 | toggleOperator(field); |
|
212 | 212 | } else { |
|
213 | 213 | $("#operators_" + fieldId).hide().attr('disabled', true); |
|
214 | 214 | enableValues(field, []); |
|
215 | 215 | } |
|
216 | 216 | } |
|
217 | 217 | |
|
218 | 218 | function enableValues(field, indexes) { |
|
219 | 219 | var fieldId = field.replace('.', '_'); |
|
220 | 220 | $('#tr_'+fieldId+' td.values .value').each(function(index) { |
|
221 | 221 | if ($.inArray(index, indexes) >= 0) { |
|
222 | 222 | $(this).removeAttr('disabled'); |
|
223 | 223 | $(this).parents('span').first().show(); |
|
224 | 224 | } else { |
|
225 | 225 | $(this).val(''); |
|
226 | 226 | $(this).attr('disabled', true); |
|
227 | 227 | $(this).parents('span').first().hide(); |
|
228 | 228 | } |
|
229 | 229 | |
|
230 | 230 | if ($(this).hasClass('group')) { |
|
231 | 231 | $(this).addClass('open'); |
|
232 | 232 | } else { |
|
233 | 233 | $(this).show(); |
|
234 | 234 | } |
|
235 | 235 | }); |
|
236 | 236 | } |
|
237 | 237 | |
|
238 | 238 | function toggleOperator(field) { |
|
239 | 239 | var fieldId = field.replace('.', '_'); |
|
240 | 240 | var operator = $("#operators_" + fieldId); |
|
241 | 241 | switch (operator.val()) { |
|
242 | 242 | case "!*": |
|
243 | 243 | case "*": |
|
244 | 244 | case "t": |
|
245 | 245 | case "ld": |
|
246 | 246 | case "w": |
|
247 | 247 | case "lw": |
|
248 | 248 | case "l2w": |
|
249 | 249 | case "m": |
|
250 | 250 | case "lm": |
|
251 | 251 | case "y": |
|
252 | 252 | case "o": |
|
253 | 253 | case "c": |
|
254 | 254 | enableValues(field, []); |
|
255 | 255 | break; |
|
256 | 256 | case "><": |
|
257 | 257 | enableValues(field, [0,1]); |
|
258 | 258 | break; |
|
259 | 259 | case "<t+": |
|
260 | 260 | case ">t+": |
|
261 | 261 | case "><t+": |
|
262 | 262 | case "t+": |
|
263 | 263 | case ">t-": |
|
264 | 264 | case "<t-": |
|
265 | 265 | case "><t-": |
|
266 | 266 | case "t-": |
|
267 | 267 | enableValues(field, [2]); |
|
268 | 268 | break; |
|
269 | 269 | case "=p": |
|
270 | 270 | case "=!p": |
|
271 | 271 | case "!p": |
|
272 | 272 | enableValues(field, [1]); |
|
273 | 273 | break; |
|
274 | 274 | default: |
|
275 | 275 | enableValues(field, [0]); |
|
276 | 276 | break; |
|
277 | 277 | } |
|
278 | 278 | } |
|
279 | 279 | |
|
280 | 280 | function toggleMultiSelect(el) { |
|
281 | 281 | if (el.attr('multiple')) { |
|
282 | 282 | el.removeAttr('multiple'); |
|
283 | 283 | } else { |
|
284 | 284 | el.attr('multiple', true); |
|
285 | 285 | } |
|
286 | 286 | } |
|
287 | 287 | |
|
288 | 288 | function submit_query_form(id) { |
|
289 | 289 | selectAllOptions("selected_columns"); |
|
290 | 290 | $('#'+id).submit(); |
|
291 | 291 | } |
|
292 | 292 | |
|
293 | var fileFieldCount = 1; | |
|
294 | function addFileField() { | |
|
295 | var fields = $('#attachments_fields'); | |
|
296 | if (fields.children().length >= 10) return false; | |
|
297 | fileFieldCount++; | |
|
298 | var s = fields.children('span').first().clone(); | |
|
299 | s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val(''); | |
|
300 | s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val(''); | |
|
301 | fields.append(s); | |
|
302 | } | |
|
303 | ||
|
304 | function removeFileField(el) { | |
|
305 | var fields = $('#attachments_fields'); | |
|
306 | var s = $(el).parents('span').first(); | |
|
307 | if (fields.children().length > 1) { | |
|
308 | s.remove(); | |
|
309 | } else { | |
|
310 | s.children('input.file').val(''); | |
|
311 | s.children('input.description').val(''); | |
|
312 | } | |
|
313 | } | |
|
314 | ||
|
315 | function checkFileSize(el, maxSize, message) { | |
|
316 | var files = el.files; | |
|
317 | if (files) { | |
|
318 | for (var i=0; i<files.length; i++) { | |
|
319 | if (files[i].size > maxSize) { | |
|
320 | alert(message); | |
|
321 | el.value = ""; | |
|
322 | } | |
|
323 | } | |
|
324 | } | |
|
325 | } | |
|
326 | ||
|
327 | 293 | function showTab(name) { |
|
328 | 294 | $('div#content .tab-content').hide(); |
|
329 | 295 | $('div.tabs a').removeClass('selected'); |
|
330 | 296 | $('#tab-content-' + name).show(); |
|
331 | 297 | $('#tab-' + name).addClass('selected'); |
|
332 | 298 | return false; |
|
333 | 299 | } |
|
334 | 300 | |
|
335 | 301 | function moveTabRight(el) { |
|
336 | 302 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
337 | 303 | var tabsWidth = 0; |
|
338 | 304 | var i = 0; |
|
339 | 305 | lis.each(function(){ |
|
340 | 306 | if ($(this).is(':visible')) { |
|
341 | 307 | tabsWidth += $(this).width() + 6; |
|
342 | 308 | } |
|
343 | 309 | }); |
|
344 | 310 | if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; } |
|
345 | 311 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
346 | 312 | lis.eq(i).hide(); |
|
347 | 313 | } |
|
348 | 314 | |
|
349 | 315 | function moveTabLeft(el) { |
|
350 | 316 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
351 | 317 | var i = 0; |
|
352 | 318 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
353 | 319 | if (i>0) { |
|
354 | 320 | lis.eq(i-1).show(); |
|
355 | 321 | } |
|
356 | 322 | } |
|
357 | 323 | |
|
358 | 324 | function displayTabsButtons() { |
|
359 | 325 | var lis; |
|
360 | 326 | var tabsWidth = 0; |
|
361 | 327 | var el; |
|
362 | 328 | $('div.tabs').each(function() { |
|
363 | 329 | el = $(this); |
|
364 | 330 | lis = el.find('ul').children(); |
|
365 | 331 | lis.each(function(){ |
|
366 | 332 | if ($(this).is(':visible')) { |
|
367 | 333 | tabsWidth += $(this).width() + 6; |
|
368 | 334 | } |
|
369 | 335 | }); |
|
370 | 336 | if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) { |
|
371 | 337 | el.find('div.tabs-buttons').hide(); |
|
372 | 338 | } else { |
|
373 | 339 | el.find('div.tabs-buttons').show(); |
|
374 | 340 | } |
|
375 | 341 | }); |
|
376 | 342 | } |
|
377 | 343 | |
|
378 | 344 | function setPredecessorFieldsVisibility() { |
|
379 | 345 | var relationType = $('#relation_relation_type'); |
|
380 | 346 | if (relationType.val() == "precedes" || relationType.val() == "follows") { |
|
381 | 347 | $('#predecessor_fields').show(); |
|
382 | 348 | } else { |
|
383 | 349 | $('#predecessor_fields').hide(); |
|
384 | 350 | } |
|
385 | 351 | } |
|
386 | 352 | |
|
387 | 353 | function showModal(id, width) { |
|
388 | 354 | var el = $('#'+id).first(); |
|
389 | 355 | if (el.length == 0 || el.is(':visible')) {return;} |
|
390 | 356 | var title = el.find('h3.title').text(); |
|
391 | 357 | el.dialog({ |
|
392 | 358 | width: width, |
|
393 | 359 | modal: true, |
|
394 | 360 | resizable: false, |
|
395 | 361 | dialogClass: 'modal', |
|
396 | 362 | title: title |
|
397 | 363 | }); |
|
398 | 364 | el.find("input[type=text], input[type=submit]").first().focus(); |
|
399 | 365 | } |
|
400 | 366 | |
|
401 | 367 | function hideModal(el) { |
|
402 | 368 | var modal; |
|
403 | 369 | if (el) { |
|
404 | 370 | modal = $(el).parents('.ui-dialog-content'); |
|
405 | 371 | } else { |
|
406 | 372 | modal = $('#ajax-modal'); |
|
407 | 373 | } |
|
408 | 374 | modal.dialog("close"); |
|
409 | 375 | } |
|
410 | 376 | |
|
411 | 377 | function submitPreview(url, form, target) { |
|
412 | 378 | $.ajax({ |
|
413 | 379 | url: url, |
|
414 | 380 | type: 'post', |
|
415 | 381 | data: $('#'+form).serialize(), |
|
416 | 382 | success: function(data){ |
|
417 | 383 | $('#'+target).html(data); |
|
418 | 384 | } |
|
419 | 385 | }); |
|
420 | 386 | } |
|
421 | 387 | |
|
422 | 388 | function collapseScmEntry(id) { |
|
423 | 389 | $('.'+id).each(function() { |
|
424 | 390 | if ($(this).hasClass('open')) { |
|
425 | 391 | collapseScmEntry($(this).attr('id')); |
|
426 | 392 | } |
|
427 | 393 | $(this).hide(); |
|
428 | 394 | }); |
|
429 | 395 | $('#'+id).removeClass('open'); |
|
430 | 396 | } |
|
431 | 397 | |
|
432 | 398 | function expandScmEntry(id) { |
|
433 | 399 | $('.'+id).each(function() { |
|
434 | 400 | $(this).show(); |
|
435 | 401 | if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) { |
|
436 | 402 | expandScmEntry($(this).attr('id')); |
|
437 | 403 | } |
|
438 | 404 | }); |
|
439 | 405 | $('#'+id).addClass('open'); |
|
440 | 406 | } |
|
441 | 407 | |
|
442 | 408 | function scmEntryClick(id, url) { |
|
443 | 409 | el = $('#'+id); |
|
444 | 410 | if (el.hasClass('open')) { |
|
445 | 411 | collapseScmEntry(id); |
|
446 | 412 | el.addClass('collapsed'); |
|
447 | 413 | return false; |
|
448 | 414 | } else if (el.hasClass('loaded')) { |
|
449 | 415 | expandScmEntry(id); |
|
450 | 416 | el.removeClass('collapsed'); |
|
451 | 417 | return false; |
|
452 | 418 | } |
|
453 | 419 | if (el.hasClass('loading')) { |
|
454 | 420 | return false; |
|
455 | 421 | } |
|
456 | 422 | el.addClass('loading'); |
|
457 | 423 | $.ajax({ |
|
458 | 424 | url: url, |
|
459 | 425 | success: function(data){ |
|
460 | 426 | el.after(data); |
|
461 | 427 | el.addClass('open').addClass('loaded').removeClass('loading'); |
|
462 | 428 | } |
|
463 | 429 | }); |
|
464 | 430 | return true; |
|
465 | 431 | } |
|
466 | 432 | |
|
467 | 433 | function randomKey(size) { |
|
468 | 434 | var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); |
|
469 | 435 | var key = ''; |
|
470 | 436 | for (i = 0; i < size; i++) { |
|
471 | 437 | key += chars[Math.floor(Math.random() * chars.length)]; |
|
472 | 438 | } |
|
473 | 439 | return key; |
|
474 | 440 | } |
|
475 | 441 | |
|
476 | 442 | // Can't use Rails' remote select because we need the form data |
|
477 | 443 | function updateIssueFrom(url) { |
|
478 | 444 | $.ajax({ |
|
479 | 445 | url: url, |
|
480 | 446 | type: 'post', |
|
481 | 447 | data: $('#issue-form').serialize() |
|
482 | 448 | }); |
|
483 | 449 | } |
|
484 | 450 | |
|
485 | 451 | function updateBulkEditFrom(url) { |
|
486 | 452 | $.ajax({ |
|
487 | 453 | url: url, |
|
488 | 454 | type: 'post', |
|
489 | 455 | data: $('#bulk_edit_form').serialize() |
|
490 | 456 | }); |
|
491 | 457 | } |
|
492 | 458 | |
|
493 | 459 | function observeAutocompleteField(fieldId, url) { |
|
494 | 460 | $(document).ready(function() { |
|
495 | 461 | $('#'+fieldId).autocomplete({ |
|
496 | 462 | source: url, |
|
497 | 463 | minLength: 2 |
|
498 | 464 | }); |
|
499 | 465 | }); |
|
500 | 466 | } |
|
501 | 467 | |
|
502 | 468 | function observeSearchfield(fieldId, targetId, url) { |
|
503 | 469 | $('#'+fieldId).each(function() { |
|
504 | 470 | var $this = $(this); |
|
505 | 471 | $this.attr('data-value-was', $this.val()); |
|
506 | 472 | var check = function() { |
|
507 | 473 | var val = $this.val(); |
|
508 | 474 | if ($this.attr('data-value-was') != val){ |
|
509 | 475 | $this.attr('data-value-was', val); |
|
510 | 476 | $.ajax({ |
|
511 | 477 | url: url, |
|
512 | 478 | type: 'get', |
|
513 | 479 | data: {q: $this.val()}, |
|
514 | 480 | success: function(data){ $('#'+targetId).html(data); }, |
|
515 | 481 | beforeSend: function(){ $this.addClass('ajax-loading'); }, |
|
516 | 482 | complete: function(){ $this.removeClass('ajax-loading'); } |
|
517 | 483 | }); |
|
518 | 484 | } |
|
519 | 485 | }; |
|
520 | 486 | var reset = function() { |
|
521 | 487 | if (timer) { |
|
522 | 488 | clearInterval(timer); |
|
523 | 489 | timer = setInterval(check, 300); |
|
524 | 490 | } |
|
525 | 491 | }; |
|
526 | 492 | var timer = setInterval(check, 300); |
|
527 | 493 | $this.bind('keyup click mousemove', reset); |
|
528 | 494 | }); |
|
529 | 495 | } |
|
530 | 496 | |
|
531 | 497 | function observeProjectModules() { |
|
532 | 498 | var f = function() { |
|
533 | 499 | /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ |
|
534 | 500 | if ($('#project_enabled_module_names_issue_tracking').attr('checked')) { |
|
535 | 501 | $('#project_trackers').show(); |
|
536 | 502 | }else{ |
|
537 | 503 | $('#project_trackers').hide(); |
|
538 | 504 | } |
|
539 | 505 | }; |
|
540 | 506 | |
|
541 | 507 | $(window).load(f); |
|
542 | 508 | $('#project_enabled_module_names_issue_tracking').change(f); |
|
543 | 509 | } |
|
544 | 510 | |
|
545 | 511 | function initMyPageSortable(list, url) { |
|
546 | 512 | $('#list-'+list).sortable({ |
|
547 | 513 | connectWith: '.block-receiver', |
|
548 | 514 | tolerance: 'pointer', |
|
549 | 515 | update: function(){ |
|
550 | 516 | $.ajax({ |
|
551 | 517 | url: url, |
|
552 | 518 | type: 'post', |
|
553 | 519 | data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})} |
|
554 | 520 | }); |
|
555 | 521 | } |
|
556 | 522 | }); |
|
557 | 523 | $("#list-top, #list-left, #list-right").disableSelection(); |
|
558 | 524 | } |
|
559 | 525 | |
|
560 | 526 | var warnLeavingUnsavedMessage; |
|
561 | 527 | function warnLeavingUnsaved(message) { |
|
562 | 528 | warnLeavingUnsavedMessage = message; |
|
563 | 529 | |
|
564 | 530 | $('form').submit(function(){ |
|
565 | 531 | $('textarea').removeData('changed'); |
|
566 | 532 | }); |
|
567 | 533 | $('textarea').change(function(){ |
|
568 | 534 | $(this).data('changed', 'changed'); |
|
569 | 535 | }); |
|
570 | 536 | window.onbeforeunload = function(){ |
|
571 | 537 | var warn = false; |
|
572 | 538 | $('textarea').blur().each(function(){ |
|
573 | 539 | if ($(this).data('changed')) { |
|
574 | 540 | warn = true; |
|
575 | 541 | } |
|
576 | 542 | }); |
|
577 | 543 | if (warn) {return warnLeavingUnsavedMessage;} |
|
578 | 544 | }; |
|
579 | 545 | }; |
|
580 | 546 | |
|
581 | 547 | $(document).ready(function(){ |
|
582 | $('#ajax-indicator').bind('ajaxSend', function(){ | |
|
583 | if ($('.ajax-loading').length == 0) { | |
|
548 | $('#ajax-indicator').bind('ajaxSend', function(event, xhr, settings){ | |
|
549 | if ($('.ajax-loading').length == 0 && settings.contentType != 'application/octet-stream') { | |
|
584 | 550 | $('#ajax-indicator').show(); |
|
585 | 551 | } |
|
586 | 552 | }); |
|
587 | 553 | $('#ajax-indicator').bind('ajaxStop', function(){ |
|
588 | 554 | $('#ajax-indicator').hide(); |
|
589 | 555 | }); |
|
590 | 556 | }); |
|
591 | 557 | |
|
592 | 558 | function hideOnLoad() { |
|
593 | 559 | $('.hol').hide(); |
|
594 | 560 | } |
|
595 | 561 | |
|
596 | 562 | function addFormObserversForDoubleSubmit() { |
|
597 | 563 | $('form[method=post]').each(function() { |
|
598 | 564 | if (!$(this).hasClass('multiple-submit')) { |
|
599 | 565 | $(this).submit(function(form_submission) { |
|
600 | 566 | if ($(form_submission.target).attr('data-submitted')) { |
|
601 | 567 | form_submission.preventDefault(); |
|
602 | 568 | } else { |
|
603 | 569 | $(form_submission.target).attr('data-submitted', true); |
|
604 | 570 | } |
|
605 | 571 | }); |
|
606 | 572 | } |
|
607 | 573 | }); |
|
608 | 574 | } |
|
609 | 575 | |
|
576 | function blockEventPropagation(event) { | |
|
577 | event.stopPropagation(); | |
|
578 | event.preventDefault(); | |
|
579 | } | |
|
580 | ||
|
610 | 581 | $(document).ready(hideOnLoad); |
|
611 | 582 | $(document).ready(addFormObserversForDoubleSubmit); |
@@ -1,1140 +1,1147 | |||
|
1 | 1 | html {overflow-y:scroll;} |
|
2 | 2 | body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } |
|
3 | 3 | |
|
4 | 4 | h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;} |
|
5 | 5 | #content h1, h2, h3, h4 {color: #555;} |
|
6 | 6 | h2, .wiki h1 {font-size: 20px;} |
|
7 | 7 | h3, .wiki h2 {font-size: 16px;} |
|
8 | 8 | h4, .wiki h3 {font-size: 13px;} |
|
9 | 9 | h4 {border-bottom: 1px dotted #bbb;} |
|
10 | 10 | |
|
11 | 11 | /***** Layout *****/ |
|
12 | 12 | #wrapper {background: white;} |
|
13 | 13 | |
|
14 | 14 | #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} |
|
15 | 15 | #top-menu ul {margin: 0; padding: 0;} |
|
16 | 16 | #top-menu li { |
|
17 | 17 | float:left; |
|
18 | 18 | list-style-type:none; |
|
19 | 19 | margin: 0px 0px 0px 0px; |
|
20 | 20 | padding: 0px 0px 0px 0px; |
|
21 | 21 | white-space:nowrap; |
|
22 | 22 | } |
|
23 | 23 | #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;} |
|
24 | 24 | #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } |
|
25 | 25 | |
|
26 | 26 | #account {float:right;} |
|
27 | 27 | |
|
28 | 28 | #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} |
|
29 | 29 | #header a {color:#f8f8f8;} |
|
30 | 30 | #header h1 a.ancestor { font-size: 80%; } |
|
31 | 31 | #quick-search {float:right;} |
|
32 | 32 | |
|
33 | 33 | #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} |
|
34 | 34 | #main-menu ul {margin: 0; padding: 0;} |
|
35 | 35 | #main-menu li { |
|
36 | 36 | float:left; |
|
37 | 37 | list-style-type:none; |
|
38 | 38 | margin: 0px 2px 0px 0px; |
|
39 | 39 | padding: 0px 0px 0px 0px; |
|
40 | 40 | white-space:nowrap; |
|
41 | 41 | } |
|
42 | 42 | #main-menu li a { |
|
43 | 43 | display: block; |
|
44 | 44 | color: #fff; |
|
45 | 45 | text-decoration: none; |
|
46 | 46 | font-weight: bold; |
|
47 | 47 | margin: 0; |
|
48 | 48 | padding: 4px 10px 4px 10px; |
|
49 | 49 | } |
|
50 | 50 | #main-menu li a:hover {background:#759FCF; color:#fff;} |
|
51 | 51 | #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} |
|
52 | 52 | |
|
53 | 53 | #admin-menu ul {margin: 0; padding: 0;} |
|
54 | 54 | #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;} |
|
55 | 55 | |
|
56 | 56 | #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;} |
|
57 | 57 | #admin-menu a.projects { background-image: url(../images/projects.png); } |
|
58 | 58 | #admin-menu a.users { background-image: url(../images/user.png); } |
|
59 | 59 | #admin-menu a.groups { background-image: url(../images/group.png); } |
|
60 | 60 | #admin-menu a.roles { background-image: url(../images/database_key.png); } |
|
61 | 61 | #admin-menu a.trackers { background-image: url(../images/ticket.png); } |
|
62 | 62 | #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } |
|
63 | 63 | #admin-menu a.workflows { background-image: url(../images/ticket_go.png); } |
|
64 | 64 | #admin-menu a.custom_fields { background-image: url(../images/textfield.png); } |
|
65 | 65 | #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } |
|
66 | 66 | #admin-menu a.settings { background-image: url(../images/changeset.png); } |
|
67 | 67 | #admin-menu a.plugins { background-image: url(../images/plugin.png); } |
|
68 | 68 | #admin-menu a.info { background-image: url(../images/help.png); } |
|
69 | 69 | #admin-menu a.server_authentication { background-image: url(../images/server_key.png); } |
|
70 | 70 | |
|
71 | 71 | #main {background-color:#EEEEEE;} |
|
72 | 72 | |
|
73 | 73 | #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;} |
|
74 | 74 | * html #sidebar{ width: 22%; } |
|
75 | 75 | #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } |
|
76 | 76 | #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } |
|
77 | 77 | * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } |
|
78 | 78 | #sidebar .contextual { margin-right: 1em; } |
|
79 | 79 | |
|
80 | 80 | #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } |
|
81 | 81 | * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} |
|
82 | 82 | html>body #content { min-height: 600px; } |
|
83 | 83 | * html body #content { height: 600px; } /* IE */ |
|
84 | 84 | |
|
85 | 85 | #main.nosidebar #sidebar{ display: none; } |
|
86 | 86 | #main.nosidebar #content{ width: auto; border-right: 0; } |
|
87 | 87 | |
|
88 | 88 | #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;} |
|
89 | 89 | |
|
90 | 90 | #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } |
|
91 | 91 | #login-form table td {padding: 6px;} |
|
92 | 92 | #login-form label {font-weight: bold;} |
|
93 | 93 | #login-form input#username, #login-form input#password { width: 300px; } |
|
94 | 94 | |
|
95 | 95 | div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;} |
|
96 | 96 | div.modal h3.title {display:none;} |
|
97 | 97 | div.modal p.buttons {text-align:right; margin-bottom:0;} |
|
98 | 98 | |
|
99 | 99 | input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; } |
|
100 | 100 | |
|
101 | 101 | .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } |
|
102 | 102 | |
|
103 | 103 | /***** Links *****/ |
|
104 | 104 | a, a:link, a:visited{ color: #169; text-decoration: none; } |
|
105 | 105 | a:hover, a:active{ color: #c61a1a; text-decoration: underline;} |
|
106 | 106 | a img{ border: 0; } |
|
107 | 107 | |
|
108 | 108 | a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } |
|
109 | 109 | a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; } |
|
110 | 110 | a.user.locked, a.user.locked:link, a.user.locked:visited {color: #999;} |
|
111 | 111 | |
|
112 | 112 | #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;} |
|
113 | 113 | #sidebar a.selected:hover {text-decoration:none;} |
|
114 | 114 | #admin-menu a {line-height:1.7em;} |
|
115 | 115 | #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;} |
|
116 | 116 | |
|
117 | 117 | a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;} |
|
118 | 118 | a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;} |
|
119 | 119 | |
|
120 | 120 | a#toggle-completed-versions {color:#999;} |
|
121 | 121 | /***** Tables *****/ |
|
122 | 122 | table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } |
|
123 | 123 | table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; } |
|
124 | 124 | table.list td { vertical-align: top; padding-right:10px; } |
|
125 | 125 | table.list td.id { width: 2%; text-align: center;} |
|
126 | 126 | table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } |
|
127 | 127 | table.list td.checkbox input {padding:0px;} |
|
128 | 128 | table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; } |
|
129 | 129 | table.list td.buttons a { padding-right: 0.6em; } |
|
130 | 130 | table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } |
|
131 | 131 | |
|
132 | 132 | tr.project td.name a { white-space:nowrap; } |
|
133 | 133 | tr.project.closed, tr.project.archived { color: #aaa; } |
|
134 | 134 | tr.project.closed a, tr.project.archived a { color: #aaa; } |
|
135 | 135 | |
|
136 | 136 | tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} |
|
137 | 137 | tr.project.idnt-1 td.name {padding-left: 0.5em;} |
|
138 | 138 | tr.project.idnt-2 td.name {padding-left: 2em;} |
|
139 | 139 | tr.project.idnt-3 td.name {padding-left: 3.5em;} |
|
140 | 140 | tr.project.idnt-4 td.name {padding-left: 5em;} |
|
141 | 141 | tr.project.idnt-5 td.name {padding-left: 6.5em;} |
|
142 | 142 | tr.project.idnt-6 td.name {padding-left: 8em;} |
|
143 | 143 | tr.project.idnt-7 td.name {padding-left: 9.5em;} |
|
144 | 144 | tr.project.idnt-8 td.name {padding-left: 11em;} |
|
145 | 145 | tr.project.idnt-9 td.name {padding-left: 12.5em;} |
|
146 | 146 | |
|
147 | 147 | tr.issue { text-align: center; white-space: nowrap; } |
|
148 | 148 | tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text, tr.issue td.relations { white-space: normal; } |
|
149 | 149 | tr.issue td.subject, tr.issue td.relations { text-align: left; } |
|
150 | 150 | tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;} |
|
151 | 151 | tr.issue td.relations span {white-space: nowrap;} |
|
152 | 152 | table.issues td.description {color:#777; font-size:90%; padding:4px 4px 4px 24px; text-align:left; white-space:normal;} |
|
153 | 153 | table.issues td.description pre {white-space:normal;} |
|
154 | 154 | |
|
155 | 155 | tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} |
|
156 | 156 | tr.issue.idnt-1 td.subject {padding-left: 0.5em;} |
|
157 | 157 | tr.issue.idnt-2 td.subject {padding-left: 2em;} |
|
158 | 158 | tr.issue.idnt-3 td.subject {padding-left: 3.5em;} |
|
159 | 159 | tr.issue.idnt-4 td.subject {padding-left: 5em;} |
|
160 | 160 | tr.issue.idnt-5 td.subject {padding-left: 6.5em;} |
|
161 | 161 | tr.issue.idnt-6 td.subject {padding-left: 8em;} |
|
162 | 162 | tr.issue.idnt-7 td.subject {padding-left: 9.5em;} |
|
163 | 163 | tr.issue.idnt-8 td.subject {padding-left: 11em;} |
|
164 | 164 | tr.issue.idnt-9 td.subject {padding-left: 12.5em;} |
|
165 | 165 | |
|
166 | 166 | tr.entry { border: 1px solid #f8f8f8; } |
|
167 | 167 | tr.entry td { white-space: nowrap; } |
|
168 | 168 | tr.entry td.filename { width: 30%; } |
|
169 | 169 | tr.entry td.filename_no_report { width: 70%; } |
|
170 | 170 | tr.entry td.size { text-align: right; font-size: 90%; } |
|
171 | 171 | tr.entry td.revision, tr.entry td.author { text-align: center; } |
|
172 | 172 | tr.entry td.age { text-align: right; } |
|
173 | 173 | tr.entry.file td.filename a { margin-left: 16px; } |
|
174 | 174 | tr.entry.file td.filename_no_report a { margin-left: 16px; } |
|
175 | 175 | |
|
176 | 176 | tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} |
|
177 | 177 | tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} |
|
178 | 178 | |
|
179 | 179 | tr.changeset { height: 20px } |
|
180 | 180 | tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } |
|
181 | 181 | tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } |
|
182 | 182 | tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} |
|
183 | 183 | tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} |
|
184 | 184 | |
|
185 | 185 | table.files tr.file td { text-align: center; } |
|
186 | 186 | table.files tr.file td.filename { text-align: left; padding-left: 24px; } |
|
187 | 187 | table.files tr.file td.digest { font-size: 80%; } |
|
188 | 188 | |
|
189 | 189 | table.members td.roles, table.memberships td.roles { width: 45%; } |
|
190 | 190 | |
|
191 | 191 | tr.message { height: 2.6em; } |
|
192 | 192 | tr.message td.subject { padding-left: 20px; } |
|
193 | 193 | tr.message td.created_on { white-space: nowrap; } |
|
194 | 194 | tr.message td.last_message { font-size: 80%; white-space: nowrap; } |
|
195 | 195 | tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; } |
|
196 | 196 | tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; } |
|
197 | 197 | |
|
198 | 198 | tr.version.closed, tr.version.closed a { color: #999; } |
|
199 | 199 | tr.version td.name { padding-left: 20px; } |
|
200 | 200 | tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; } |
|
201 | 201 | tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } |
|
202 | 202 | |
|
203 | 203 | tr.user td { width:13%; } |
|
204 | 204 | tr.user td.email { width:18%; } |
|
205 | 205 | tr.user td { white-space: nowrap; } |
|
206 | 206 | tr.user.locked, tr.user.registered { color: #aaa; } |
|
207 | 207 | tr.user.locked a, tr.user.registered a { color: #aaa; } |
|
208 | 208 | |
|
209 | 209 | table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;} |
|
210 | 210 | |
|
211 | 211 | tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;} |
|
212 | 212 | |
|
213 | 213 | tr.time-entry { text-align: center; white-space: nowrap; } |
|
214 | 214 | tr.time-entry td.issue, tr.time-entry td.comments { text-align: left; white-space: normal; } |
|
215 | 215 | td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } |
|
216 | 216 | td.hours .hours-dec { font-size: 0.9em; } |
|
217 | 217 | |
|
218 | 218 | table.plugins td { vertical-align: middle; } |
|
219 | 219 | table.plugins td.configure { text-align: right; padding-right: 1em; } |
|
220 | 220 | table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } |
|
221 | 221 | table.plugins span.description { display: block; font-size: 0.9em; } |
|
222 | 222 | table.plugins span.url { display: block; font-size: 0.9em; } |
|
223 | 223 | |
|
224 | 224 | table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } |
|
225 | 225 | table.list tbody tr.group span.count {position:relative; top:-1px; color:#fff; font-size:10px; background:#9DB9D5; padding:0px 6px 1px 6px; border-radius:3px; margin-left:4px;} |
|
226 | 226 | tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;} |
|
227 | 227 | tr.group:hover a.toggle-all { display:inline;} |
|
228 | 228 | a.toggle-all:hover {text-decoration:none;} |
|
229 | 229 | |
|
230 | 230 | table.list tbody tr:hover { background-color:#ffffdd; } |
|
231 | 231 | table.list tbody tr.group:hover { background-color:inherit; } |
|
232 | 232 | table td {padding:2px;} |
|
233 | 233 | table p {margin:0;} |
|
234 | 234 | .odd {background-color:#f6f7f8;} |
|
235 | 235 | .even {background-color: #fff;} |
|
236 | 236 | |
|
237 | 237 | a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; } |
|
238 | 238 | a.sort.asc { background-image: url(../images/sort_asc.png); } |
|
239 | 239 | a.sort.desc { background-image: url(../images/sort_desc.png); } |
|
240 | 240 | |
|
241 | 241 | table.attributes { width: 100% } |
|
242 | 242 | table.attributes th { vertical-align: top; text-align: left; } |
|
243 | 243 | table.attributes td { vertical-align: top; } |
|
244 | 244 | |
|
245 | 245 | table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; } |
|
246 | 246 | table.boards td.topic-count, table.boards td.message-count {text-align:center;} |
|
247 | 247 | table.boards td.last-message {font-size:80%;} |
|
248 | 248 | |
|
249 | 249 | table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;} |
|
250 | 250 | |
|
251 | 251 | table.query-columns { |
|
252 | 252 | border-collapse: collapse; |
|
253 | 253 | border: 0; |
|
254 | 254 | } |
|
255 | 255 | |
|
256 | 256 | table.query-columns td.buttons { |
|
257 | 257 | vertical-align: middle; |
|
258 | 258 | text-align: center; |
|
259 | 259 | } |
|
260 | 260 | |
|
261 | 261 | td.center {text-align:center;} |
|
262 | 262 | |
|
263 | 263 | h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } |
|
264 | 264 | |
|
265 | 265 | div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } |
|
266 | 266 | div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } |
|
267 | 267 | div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } |
|
268 | 268 | div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } |
|
269 | 269 | |
|
270 | 270 | #watchers ul {margin: 0; padding: 0;} |
|
271 | 271 | #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} |
|
272 | 272 | #watchers select {width: 95%; display: block;} |
|
273 | 273 | #watchers a.delete {opacity: 0.4;} |
|
274 | 274 | #watchers a.delete:hover {opacity: 1;} |
|
275 | 275 | #watchers img.gravatar {margin: 0 4px 2px 0;} |
|
276 | 276 | |
|
277 | 277 | span#watchers_inputs {overflow:auto; display:block;} |
|
278 | 278 | span.search_for_watchers {display:block;} |
|
279 | 279 | span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;} |
|
280 | 280 | span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; } |
|
281 | 281 | |
|
282 | 282 | |
|
283 | 283 | .highlight { background-color: #FCFD8D;} |
|
284 | 284 | .highlight.token-1 { background-color: #faa;} |
|
285 | 285 | .highlight.token-2 { background-color: #afa;} |
|
286 | 286 | .highlight.token-3 { background-color: #aaf;} |
|
287 | 287 | |
|
288 | 288 | .box{ |
|
289 | 289 | padding:6px; |
|
290 | 290 | margin-bottom: 10px; |
|
291 | 291 | background-color:#f6f6f6; |
|
292 | 292 | color:#505050; |
|
293 | 293 | line-height:1.5em; |
|
294 | 294 | border: 1px solid #e4e4e4; |
|
295 | 295 | } |
|
296 | 296 | |
|
297 | 297 | div.square { |
|
298 | 298 | border: 1px solid #999; |
|
299 | 299 | float: left; |
|
300 | 300 | margin: .3em .4em 0 .4em; |
|
301 | 301 | overflow: hidden; |
|
302 | 302 | width: .6em; height: .6em; |
|
303 | 303 | } |
|
304 | 304 | .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} |
|
305 | 305 | .contextual input, .contextual select {font-size:0.9em;} |
|
306 | 306 | .message .contextual { margin-top: 0; } |
|
307 | 307 | |
|
308 | 308 | .splitcontent {overflow:auto;} |
|
309 | 309 | .splitcontentleft{float:left; width:49%;} |
|
310 | 310 | .splitcontentright{float:right; width:49%;} |
|
311 | 311 | form {display: inline;} |
|
312 | 312 | input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} |
|
313 | 313 | fieldset {border: 1px solid #e4e4e4; margin:0;} |
|
314 | 314 | legend {color: #484848;} |
|
315 | 315 | hr { width: 100%; height: 1px; background: #ccc; border: 0;} |
|
316 | 316 | blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} |
|
317 | 317 | blockquote blockquote { margin-left: 0;} |
|
318 | 318 | acronym { border-bottom: 1px dotted; cursor: help; } |
|
319 | 319 | textarea.wiki-edit {width:99%; resize:vertical;} |
|
320 | 320 | li p {margin-top: 0;} |
|
321 | 321 | div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} |
|
322 | 322 | p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} |
|
323 | 323 | p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } |
|
324 | 324 | p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } |
|
325 | 325 | |
|
326 | 326 | div.issue div.subject div div { padding-left: 16px; } |
|
327 | 327 | div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;} |
|
328 | 328 | div.issue div.subject>div>p { margin-top: 0.5em; } |
|
329 | 329 | div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;} |
|
330 | 330 | div.issue 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;} |
|
331 | 331 | div.issue .next-prev-links {color:#999;} |
|
332 | 332 | div.issue table.attributes th {width:22%;} |
|
333 | 333 | div.issue table.attributes td {width:28%;} |
|
334 | 334 | |
|
335 | 335 | #issue_tree table.issues, #relations table.issues { border: 0; } |
|
336 | 336 | #issue_tree td.checkbox, #relations td.checkbox {display:none;} |
|
337 | 337 | #relations td.buttons {padding:0;} |
|
338 | 338 | |
|
339 | 339 | fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } |
|
340 | 340 | fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } |
|
341 | 341 | fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } |
|
342 | 342 | |
|
343 | 343 | fieldset#date-range p { margin: 2px 0 2px 0; } |
|
344 | 344 | fieldset#filters table { border-collapse: collapse; } |
|
345 | 345 | fieldset#filters table td { padding: 0; vertical-align: middle; } |
|
346 | 346 | fieldset#filters tr.filter { height: 2.1em; } |
|
347 | 347 | fieldset#filters td.field { width:230px; } |
|
348 | 348 | fieldset#filters td.operator { width:180px; } |
|
349 | 349 | fieldset#filters td.operator select {max-width:170px;} |
|
350 | 350 | fieldset#filters td.values { white-space:nowrap; } |
|
351 | 351 | fieldset#filters td.values select {min-width:130px;} |
|
352 | 352 | fieldset#filters td.values input {height:1em;} |
|
353 | 353 | fieldset#filters td.add-filter { text-align: right; vertical-align: top; } |
|
354 | 354 | |
|
355 | 355 | .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;} |
|
356 | 356 | .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } |
|
357 | 357 | |
|
358 | 358 | div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} |
|
359 | 359 | div#issue-changesets div.changeset { padding: 4px;} |
|
360 | 360 | div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } |
|
361 | 361 | div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} |
|
362 | 362 | |
|
363 | 363 | .journal ul.details img {margin:0 0 -3px 4px;} |
|
364 | 364 | div.journal {overflow:auto;} |
|
365 | 365 | div.journal.private-notes {border-left:2px solid #d22; padding-left:4px; margin-left:-6px;} |
|
366 | 366 | |
|
367 | 367 | div#activity dl, #search-results { margin-left: 2em; } |
|
368 | 368 | div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } |
|
369 | 369 | div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; } |
|
370 | 370 | div#activity dt.me .time { border-bottom: 1px solid #999; } |
|
371 | 371 | div#activity dt .time { color: #777; font-size: 80%; } |
|
372 | 372 | div#activity dd .description, #search-results dd .description { font-style: italic; } |
|
373 | 373 | div#activity span.project:after, #search-results span.project:after { content: " -"; } |
|
374 | 374 | div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } |
|
375 | 375 | div#activity dt.grouped {margin-left:5em;} |
|
376 | 376 | div#activity dd.grouped {margin-left:9em;} |
|
377 | 377 | |
|
378 | 378 | #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } |
|
379 | 379 | |
|
380 | 380 | div#search-results-counts {float:right;} |
|
381 | 381 | div#search-results-counts ul { margin-top: 0.5em; } |
|
382 | 382 | div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } |
|
383 | 383 | |
|
384 | 384 | dt.issue { background-image: url(../images/ticket.png); } |
|
385 | 385 | dt.issue-edit { background-image: url(../images/ticket_edit.png); } |
|
386 | 386 | dt.issue-closed { background-image: url(../images/ticket_checked.png); } |
|
387 | 387 | dt.issue-note { background-image: url(../images/ticket_note.png); } |
|
388 | 388 | dt.changeset { background-image: url(../images/changeset.png); } |
|
389 | 389 | dt.news { background-image: url(../images/news.png); } |
|
390 | 390 | dt.message { background-image: url(../images/message.png); } |
|
391 | 391 | dt.reply { background-image: url(../images/comments.png); } |
|
392 | 392 | dt.wiki-page { background-image: url(../images/wiki_edit.png); } |
|
393 | 393 | dt.attachment { background-image: url(../images/attachment.png); } |
|
394 | 394 | dt.document { background-image: url(../images/document.png); } |
|
395 | 395 | dt.project { background-image: url(../images/projects.png); } |
|
396 | 396 | dt.time-entry { background-image: url(../images/time.png); } |
|
397 | 397 | |
|
398 | 398 | #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } |
|
399 | 399 | |
|
400 | 400 | div#roadmap .related-issues { margin-bottom: 1em; } |
|
401 | 401 | div#roadmap .related-issues td.checkbox { display: none; } |
|
402 | 402 | div#roadmap .wiki h1:first-child { display: none; } |
|
403 | 403 | div#roadmap .wiki h1 { font-size: 120%; } |
|
404 | 404 | div#roadmap .wiki h2 { font-size: 110%; } |
|
405 | 405 | body.controller-versions.action-show div#roadmap .related-issues {width:70%;} |
|
406 | 406 | |
|
407 | 407 | div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } |
|
408 | 408 | div#version-summary fieldset { margin-bottom: 1em; } |
|
409 | 409 | div#version-summary fieldset.time-tracking table { width:100%; } |
|
410 | 410 | div#version-summary th, div#version-summary td.total-hours { text-align: right; } |
|
411 | 411 | |
|
412 | 412 | table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } |
|
413 | 413 | table#time-report tbody tr.subtotal { font-style: italic; color:#777;} |
|
414 | 414 | table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; } |
|
415 | 415 | table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;} |
|
416 | 416 | table#time-report .hours-dec { font-size: 0.9em; } |
|
417 | 417 | |
|
418 | 418 | div.wiki-page .contextual a {opacity: 0.4} |
|
419 | 419 | div.wiki-page .contextual a:hover {opacity: 1} |
|
420 | 420 | |
|
421 | 421 | form .attributes select { width: 60%; } |
|
422 | 422 | input#issue_subject { width: 99%; } |
|
423 | 423 | select#issue_done_ratio { width: 95px; } |
|
424 | 424 | |
|
425 | 425 | ul.projects {margin:0; padding-left:1em;} |
|
426 | 426 | ul.projects ul {padding-left:1.6em;} |
|
427 | 427 | ul.projects.root {margin:0; padding:0;} |
|
428 | 428 | ul.projects li {list-style-type:none;} |
|
429 | 429 | |
|
430 | 430 | #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;} |
|
431 | 431 | #projects-index ul.projects li.root {margin-bottom: 1em;} |
|
432 | 432 | #projects-index ul.projects li.child {margin-top: 1em;} |
|
433 | 433 | #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; } |
|
434 | 434 | .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } |
|
435 | 435 | |
|
436 | 436 | #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;} |
|
437 | 437 | |
|
438 | 438 | #related-issues li img {vertical-align:middle;} |
|
439 | 439 | |
|
440 | 440 | ul.properties {padding:0; font-size: 0.9em; color: #777;} |
|
441 | 441 | ul.properties li {list-style-type:none;} |
|
442 | 442 | ul.properties li span {font-style:italic;} |
|
443 | 443 | |
|
444 | 444 | .total-hours { font-size: 110%; font-weight: bold; } |
|
445 | 445 | .total-hours span.hours-int { font-size: 120%; } |
|
446 | 446 | |
|
447 | 447 | .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} |
|
448 | 448 | #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } |
|
449 | 449 | |
|
450 | 450 | #workflow_copy_form select { width: 200px; } |
|
451 | 451 | table.transitions td.enabled {background: #bfb;} |
|
452 | 452 | table.fields_permissions select {font-size:90%} |
|
453 | 453 | table.fields_permissions td.readonly {background:#ddd;} |
|
454 | 454 | table.fields_permissions td.required {background:#d88;} |
|
455 | 455 | |
|
456 | 456 | textarea#custom_field_possible_values {width: 99%} |
|
457 | 457 | input#content_comments {width: 99%} |
|
458 | 458 | |
|
459 | 459 | .pagination {font-size: 90%} |
|
460 | 460 | p.pagination {margin-top:8px;} |
|
461 | 461 | |
|
462 | 462 | /***** Tabular forms ******/ |
|
463 | 463 | .tabular p{ |
|
464 | 464 | margin: 0; |
|
465 | 465 | padding: 3px 0 3px 0; |
|
466 | 466 | padding-left: 180px; /* width of left column containing the label elements */ |
|
467 | 467 | min-height: 1.8em; |
|
468 | 468 | clear:left; |
|
469 | 469 | } |
|
470 | 470 | |
|
471 | 471 | html>body .tabular p {overflow:hidden;} |
|
472 | 472 | |
|
473 | 473 | .tabular label{ |
|
474 | 474 | font-weight: bold; |
|
475 | 475 | float: left; |
|
476 | 476 | text-align: right; |
|
477 | 477 | /* width of left column */ |
|
478 | 478 | margin-left: -180px; |
|
479 | 479 | /* width of labels. Should be smaller than left column to create some right margin */ |
|
480 | 480 | width: 175px; |
|
481 | 481 | } |
|
482 | 482 | |
|
483 | 483 | .tabular label.floating{ |
|
484 | 484 | font-weight: normal; |
|
485 | 485 | margin-left: 0px; |
|
486 | 486 | text-align: left; |
|
487 | 487 | width: 270px; |
|
488 | 488 | } |
|
489 | 489 | |
|
490 | 490 | .tabular label.block{ |
|
491 | 491 | font-weight: normal; |
|
492 | 492 | margin-left: 0px !important; |
|
493 | 493 | text-align: left; |
|
494 | 494 | float: none; |
|
495 | 495 | display: block; |
|
496 | 496 | width: auto; |
|
497 | 497 | } |
|
498 | 498 | |
|
499 | 499 | .tabular label.inline{ |
|
500 | 500 | font-weight: normal; |
|
501 | 501 | float:none; |
|
502 | 502 | margin-left: 5px !important; |
|
503 | 503 | width: auto; |
|
504 | 504 | } |
|
505 | 505 | |
|
506 | 506 | label.no-css { |
|
507 | 507 | font-weight: inherit; |
|
508 | 508 | float:none; |
|
509 | 509 | text-align:left; |
|
510 | 510 | margin-left:0px; |
|
511 | 511 | width:auto; |
|
512 | 512 | } |
|
513 | 513 | input#time_entry_comments { width: 90%;} |
|
514 | 514 | |
|
515 | 515 | #preview fieldset {margin-top: 1em; background: url(../images/draft.png)} |
|
516 | 516 | |
|
517 | 517 | .tabular.settings p{ padding-left: 300px; } |
|
518 | 518 | .tabular.settings label{ margin-left: -300px; width: 295px; } |
|
519 | 519 | .tabular.settings textarea { width: 99%; } |
|
520 | 520 | |
|
521 | 521 | .settings.enabled_scm table {width:100%} |
|
522 | 522 | .settings.enabled_scm td.scm_name{ font-weight: bold; } |
|
523 | 523 | |
|
524 | 524 | fieldset.settings label { display: block; } |
|
525 | 525 | fieldset#notified_events .parent { padding-left: 20px; } |
|
526 | 526 | |
|
527 | 527 | span.required {color: #bb0000;} |
|
528 | 528 | .summary {font-style: italic;} |
|
529 | 529 | |
|
530 |
#attachments_fields input.description {margin-left: |
|
|
530 | #attachments_fields input.description {margin-left:4px; width:340px;} | |
|
531 | 531 | #attachments_fields span {display:block; white-space:nowrap;} |
|
532 | #attachments_fields img {vertical-align: middle;} | |
|
532 | #attachments_fields input.filename {border:0; height:1.8em; width:250px; color:#555; background-color:inherit; background:url(../images/attachment.png) no-repeat 1px 50%; padding-left:18px;} | |
|
533 | #attachments_fields .ajax-waiting input.filename {background:url(../images/hourglass.png) no-repeat 0px 50%;} | |
|
534 | #attachments_fields .ajax-loading input.filename {background:url(../images/loading.gif) no-repeat 0px 50%;} | |
|
535 | #attachments_fields div.ui-progressbar { width: 100px; height:14px; margin: 2px 0 -5px 8px; display: inline-block; } | |
|
536 | a.remove-upload {background: url(../images/delete.png) no-repeat 1px 50%; width:1px; display:inline-block; padding-left:16px;} | |
|
537 | a.remove-upload:hover {text-decoration:none !important;} | |
|
538 | ||
|
539 | div.fileover { background-color: lavender; } | |
|
533 | 540 | |
|
534 | 541 | div.attachments { margin-top: 12px; } |
|
535 | 542 | div.attachments p { margin:4px 0 2px 0; } |
|
536 | 543 | div.attachments img { vertical-align: middle; } |
|
537 | 544 | div.attachments span.author { font-size: 0.9em; color: #888; } |
|
538 | 545 | |
|
539 | 546 | div.thumbnails {margin-top:0.6em;} |
|
540 | 547 | div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;} |
|
541 | 548 | div.thumbnails img {margin: 3px;} |
|
542 | 549 | |
|
543 | 550 | p.other-formats { text-align: right; font-size:0.9em; color: #666; } |
|
544 | 551 | .other-formats span + span:before { content: "| "; } |
|
545 | 552 | |
|
546 | 553 | a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } |
|
547 | 554 | |
|
548 | 555 | em.info {font-style:normal;font-size:90%;color:#888;display:block;} |
|
549 | 556 | em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;} |
|
550 | 557 | |
|
551 | 558 | textarea.text_cf {width:90%;} |
|
552 | 559 | |
|
553 | 560 | /* Project members tab */ |
|
554 | 561 | div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } |
|
555 | 562 | div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } |
|
556 | 563 | div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } |
|
557 | 564 | div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } |
|
558 | 565 | div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } |
|
559 | 566 | div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } |
|
560 | 567 | |
|
561 | 568 | #users_for_watcher {height: 200px; overflow:auto;} |
|
562 | 569 | #users_for_watcher label {display: block;} |
|
563 | 570 | |
|
564 | 571 | table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } |
|
565 | 572 | |
|
566 | 573 | input#principal_search, input#user_search {width:100%} |
|
567 | 574 | input#principal_search, input#user_search { |
|
568 | 575 | background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px; |
|
569 | 576 | border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%; |
|
570 | 577 | } |
|
571 | 578 | input#principal_search.ajax-loading, input#user_search.ajax-loading { |
|
572 | 579 | background-image: url(../images/loading.gif); |
|
573 | 580 | } |
|
574 | 581 | |
|
575 | 582 | * html div#tab-content-members fieldset div { height: 450px; } |
|
576 | 583 | |
|
577 | 584 | /***** Flash & error messages ****/ |
|
578 | 585 | #errorExplanation, div.flash, .nodata, .warning, .conflict { |
|
579 | 586 | padding: 4px 4px 4px 30px; |
|
580 | 587 | margin-bottom: 12px; |
|
581 | 588 | font-size: 1.1em; |
|
582 | 589 | border: 2px solid; |
|
583 | 590 | } |
|
584 | 591 | |
|
585 | 592 | div.flash {margin-top: 8px;} |
|
586 | 593 | |
|
587 | 594 | div.flash.error, #errorExplanation { |
|
588 | 595 | background: url(../images/exclamation.png) 8px 50% no-repeat; |
|
589 | 596 | background-color: #ffe3e3; |
|
590 | 597 | border-color: #dd0000; |
|
591 | 598 | color: #880000; |
|
592 | 599 | } |
|
593 | 600 | |
|
594 | 601 | div.flash.notice { |
|
595 | 602 | background: url(../images/true.png) 8px 5px no-repeat; |
|
596 | 603 | background-color: #dfffdf; |
|
597 | 604 | border-color: #9fcf9f; |
|
598 | 605 | color: #005f00; |
|
599 | 606 | } |
|
600 | 607 | |
|
601 | 608 | div.flash.warning, .conflict { |
|
602 | 609 | background: url(../images/warning.png) 8px 5px no-repeat; |
|
603 | 610 | background-color: #FFEBC1; |
|
604 | 611 | border-color: #FDBF3B; |
|
605 | 612 | color: #A6750C; |
|
606 | 613 | text-align: left; |
|
607 | 614 | } |
|
608 | 615 | |
|
609 | 616 | .nodata, .warning { |
|
610 | 617 | text-align: center; |
|
611 | 618 | background-color: #FFEBC1; |
|
612 | 619 | border-color: #FDBF3B; |
|
613 | 620 | color: #A6750C; |
|
614 | 621 | } |
|
615 | 622 | |
|
616 | 623 | #errorExplanation ul { font-size: 0.9em;} |
|
617 | 624 | #errorExplanation h2, #errorExplanation p { display: none; } |
|
618 | 625 | |
|
619 | 626 | .conflict-details {font-size:80%;} |
|
620 | 627 | |
|
621 | 628 | /***** Ajax indicator ******/ |
|
622 | 629 | #ajax-indicator { |
|
623 | 630 | position: absolute; /* fixed not supported by IE */ |
|
624 | 631 | background-color:#eee; |
|
625 | 632 | border: 1px solid #bbb; |
|
626 | 633 | top:35%; |
|
627 | 634 | left:40%; |
|
628 | 635 | width:20%; |
|
629 | 636 | font-weight:bold; |
|
630 | 637 | text-align:center; |
|
631 | 638 | padding:0.6em; |
|
632 | 639 | z-index:100; |
|
633 | 640 | opacity: 0.5; |
|
634 | 641 | } |
|
635 | 642 | |
|
636 | 643 | html>body #ajax-indicator { position: fixed; } |
|
637 | 644 | |
|
638 | 645 | #ajax-indicator span { |
|
639 | 646 | background-position: 0% 40%; |
|
640 | 647 | background-repeat: no-repeat; |
|
641 | 648 | background-image: url(../images/loading.gif); |
|
642 | 649 | padding-left: 26px; |
|
643 | 650 | vertical-align: bottom; |
|
644 | 651 | } |
|
645 | 652 | |
|
646 | 653 | /***** Calendar *****/ |
|
647 | 654 | table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} |
|
648 | 655 | table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; } |
|
649 | 656 | table.cal thead th.week-number {width: auto;} |
|
650 | 657 | table.cal tbody tr {height: 100px;} |
|
651 | 658 | table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} |
|
652 | 659 | table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;} |
|
653 | 660 | table.cal td p.day-num {font-size: 1.1em; text-align:right;} |
|
654 | 661 | table.cal td.odd p.day-num {color: #bbb;} |
|
655 | 662 | table.cal td.today {background:#ffffdd;} |
|
656 | 663 | table.cal td.today p.day-num {font-weight: bold;} |
|
657 | 664 | table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;} |
|
658 | 665 | table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;} |
|
659 | 666 | table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;} |
|
660 | 667 | p.cal.legend span {display:block;} |
|
661 | 668 | |
|
662 | 669 | /***** Tooltips ******/ |
|
663 | 670 | .tooltip{position:relative;z-index:24;} |
|
664 | 671 | .tooltip:hover{z-index:25;color:#000;} |
|
665 | 672 | .tooltip span.tip{display: none; text-align:left;} |
|
666 | 673 | |
|
667 | 674 | div.tooltip:hover span.tip{ |
|
668 | 675 | display:block; |
|
669 | 676 | position:absolute; |
|
670 | 677 | top:12px; left:24px; width:270px; |
|
671 | 678 | border:1px solid #555; |
|
672 | 679 | background-color:#fff; |
|
673 | 680 | padding: 4px; |
|
674 | 681 | font-size: 0.8em; |
|
675 | 682 | color:#505050; |
|
676 | 683 | } |
|
677 | 684 | |
|
678 | 685 | img.ui-datepicker-trigger { |
|
679 | 686 | cursor: pointer; |
|
680 | 687 | vertical-align: middle; |
|
681 | 688 | margin-left: 4px; |
|
682 | 689 | } |
|
683 | 690 | |
|
684 | 691 | /***** Progress bar *****/ |
|
685 | 692 | table.progress { |
|
686 | 693 | border-collapse: collapse; |
|
687 | 694 | border-spacing: 0pt; |
|
688 | 695 | empty-cells: show; |
|
689 | 696 | text-align: center; |
|
690 | 697 | float:left; |
|
691 | 698 | margin: 1px 6px 1px 0px; |
|
692 | 699 | } |
|
693 | 700 | |
|
694 | 701 | table.progress td { height: 1em; } |
|
695 | 702 | table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } |
|
696 | 703 | table.progress td.done { background: #D3EDD3 none repeat scroll 0%; } |
|
697 | 704 | table.progress td.todo { background: #eee none repeat scroll 0%; } |
|
698 | 705 | p.pourcent {font-size: 80%;} |
|
699 | 706 | p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;} |
|
700 | 707 | |
|
701 | 708 | #roadmap table.progress td { height: 1.2em; } |
|
702 | 709 | /***** Tabs *****/ |
|
703 | 710 | #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;} |
|
704 | 711 | #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;} |
|
705 | 712 | #content .tabs ul li { |
|
706 | 713 | float:left; |
|
707 | 714 | list-style-type:none; |
|
708 | 715 | white-space:nowrap; |
|
709 | 716 | margin-right:4px; |
|
710 | 717 | background:#fff; |
|
711 | 718 | position:relative; |
|
712 | 719 | margin-bottom:-1px; |
|
713 | 720 | } |
|
714 | 721 | #content .tabs ul li a{ |
|
715 | 722 | display:block; |
|
716 | 723 | font-size: 0.9em; |
|
717 | 724 | text-decoration:none; |
|
718 | 725 | line-height:1.3em; |
|
719 | 726 | padding:4px 6px 4px 6px; |
|
720 | 727 | border: 1px solid #ccc; |
|
721 | 728 | border-bottom: 1px solid #bbbbbb; |
|
722 | 729 | background-color: #f6f6f6; |
|
723 | 730 | color:#999; |
|
724 | 731 | font-weight:bold; |
|
725 | 732 | border-top-left-radius:3px; |
|
726 | 733 | border-top-right-radius:3px; |
|
727 | 734 | } |
|
728 | 735 | |
|
729 | 736 | #content .tabs ul li a:hover { |
|
730 | 737 | background-color: #ffffdd; |
|
731 | 738 | text-decoration:none; |
|
732 | 739 | } |
|
733 | 740 | |
|
734 | 741 | #content .tabs ul li a.selected { |
|
735 | 742 | background-color: #fff; |
|
736 | 743 | border: 1px solid #bbbbbb; |
|
737 | 744 | border-bottom: 1px solid #fff; |
|
738 | 745 | color:#444; |
|
739 | 746 | } |
|
740 | 747 | |
|
741 | 748 | #content .tabs ul li a.selected:hover {background-color: #fff;} |
|
742 | 749 | |
|
743 | 750 | div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; } |
|
744 | 751 | |
|
745 | 752 | button.tab-left, button.tab-right { |
|
746 | 753 | font-size: 0.9em; |
|
747 | 754 | cursor: pointer; |
|
748 | 755 | height:24px; |
|
749 | 756 | border: 1px solid #ccc; |
|
750 | 757 | border-bottom: 1px solid #bbbbbb; |
|
751 | 758 | position:absolute; |
|
752 | 759 | padding:4px; |
|
753 | 760 | width: 20px; |
|
754 | 761 | bottom: -1px; |
|
755 | 762 | } |
|
756 | 763 | |
|
757 | 764 | button.tab-left { |
|
758 | 765 | right: 20px; |
|
759 | 766 | background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%; |
|
760 | 767 | border-top-left-radius:3px; |
|
761 | 768 | } |
|
762 | 769 | |
|
763 | 770 | button.tab-right { |
|
764 | 771 | right: 0; |
|
765 | 772 | background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%; |
|
766 | 773 | border-top-right-radius:3px; |
|
767 | 774 | } |
|
768 | 775 | |
|
769 | 776 | /***** Diff *****/ |
|
770 | 777 | .diff_out { background: #fcc; } |
|
771 | 778 | .diff_out span { background: #faa; } |
|
772 | 779 | .diff_in { background: #cfc; } |
|
773 | 780 | .diff_in span { background: #afa; } |
|
774 | 781 | |
|
775 | 782 | .text-diff { |
|
776 | 783 | padding: 1em; |
|
777 | 784 | background-color:#f6f6f6; |
|
778 | 785 | color:#505050; |
|
779 | 786 | border: 1px solid #e4e4e4; |
|
780 | 787 | } |
|
781 | 788 | |
|
782 | 789 | /***** Wiki *****/ |
|
783 | 790 | div.wiki table { |
|
784 | 791 | border-collapse: collapse; |
|
785 | 792 | margin-bottom: 1em; |
|
786 | 793 | } |
|
787 | 794 | |
|
788 | 795 | div.wiki table, div.wiki td, div.wiki th { |
|
789 | 796 | border: 1px solid #bbb; |
|
790 | 797 | padding: 4px; |
|
791 | 798 | } |
|
792 | 799 | |
|
793 | 800 | div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;} |
|
794 | 801 | |
|
795 | 802 | div.wiki .external { |
|
796 | 803 | background-position: 0% 60%; |
|
797 | 804 | background-repeat: no-repeat; |
|
798 | 805 | padding-left: 12px; |
|
799 | 806 | background-image: url(../images/external.png); |
|
800 | 807 | } |
|
801 | 808 | |
|
802 | 809 | div.wiki a.new {color: #b73535;} |
|
803 | 810 | |
|
804 | 811 | div.wiki ul, div.wiki ol {margin-bottom:1em;} |
|
805 | 812 | |
|
806 | 813 | div.wiki pre { |
|
807 | 814 | margin: 1em 1em 1em 1.6em; |
|
808 | 815 | padding: 8px; |
|
809 | 816 | background-color: #fafafa; |
|
810 | 817 | border: 1px solid #e2e2e2; |
|
811 | 818 | width:auto; |
|
812 | 819 | overflow-x: auto; |
|
813 | 820 | overflow-y: hidden; |
|
814 | 821 | } |
|
815 | 822 | |
|
816 | 823 | div.wiki ul.toc { |
|
817 | 824 | background-color: #ffffdd; |
|
818 | 825 | border: 1px solid #e4e4e4; |
|
819 | 826 | padding: 4px; |
|
820 | 827 | line-height: 1.2em; |
|
821 | 828 | margin-bottom: 12px; |
|
822 | 829 | margin-right: 12px; |
|
823 | 830 | margin-left: 0; |
|
824 | 831 | display: table |
|
825 | 832 | } |
|
826 | 833 | * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ |
|
827 | 834 | |
|
828 | 835 | div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } |
|
829 | 836 | div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } |
|
830 | 837 | div.wiki ul.toc ul { margin: 0; padding: 0; } |
|
831 | 838 | div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;} |
|
832 | 839 | div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;} |
|
833 | 840 | div.wiki ul.toc a { |
|
834 | 841 | font-size: 0.9em; |
|
835 | 842 | font-weight: normal; |
|
836 | 843 | text-decoration: none; |
|
837 | 844 | color: #606060; |
|
838 | 845 | } |
|
839 | 846 | div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} |
|
840 | 847 | |
|
841 | 848 | a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } |
|
842 | 849 | a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } |
|
843 | 850 | h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } |
|
844 | 851 | |
|
845 | 852 | div.wiki img { vertical-align: middle; } |
|
846 | 853 | |
|
847 | 854 | /***** My page layout *****/ |
|
848 | 855 | .block-receiver { |
|
849 | 856 | border:1px dashed #c0c0c0; |
|
850 | 857 | margin-bottom: 20px; |
|
851 | 858 | padding: 15px 0 15px 0; |
|
852 | 859 | } |
|
853 | 860 | |
|
854 | 861 | .mypage-box { |
|
855 | 862 | margin:0 0 20px 0; |
|
856 | 863 | color:#505050; |
|
857 | 864 | line-height:1.5em; |
|
858 | 865 | } |
|
859 | 866 | |
|
860 | 867 | .handle {cursor: move;} |
|
861 | 868 | |
|
862 | 869 | a.close-icon { |
|
863 | 870 | display:block; |
|
864 | 871 | margin-top:3px; |
|
865 | 872 | overflow:hidden; |
|
866 | 873 | width:12px; |
|
867 | 874 | height:12px; |
|
868 | 875 | background-repeat: no-repeat; |
|
869 | 876 | cursor:pointer; |
|
870 | 877 | background-image:url('../images/close.png'); |
|
871 | 878 | } |
|
872 | 879 | a.close-icon:hover {background-image:url('../images/close_hl.png');} |
|
873 | 880 | |
|
874 | 881 | /***** Gantt chart *****/ |
|
875 | 882 | .gantt_hdr { |
|
876 | 883 | position:absolute; |
|
877 | 884 | top:0; |
|
878 | 885 | height:16px; |
|
879 | 886 | border-top: 1px solid #c0c0c0; |
|
880 | 887 | border-bottom: 1px solid #c0c0c0; |
|
881 | 888 | border-right: 1px solid #c0c0c0; |
|
882 | 889 | text-align: center; |
|
883 | 890 | overflow: hidden; |
|
884 | 891 | } |
|
885 | 892 | |
|
886 | 893 | .gantt_hdr.nwday {background-color:#f1f1f1;} |
|
887 | 894 | |
|
888 | 895 | .gantt_subjects { font-size: 0.8em; } |
|
889 | 896 | .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } |
|
890 | 897 | |
|
891 | 898 | .task { |
|
892 | 899 | position: absolute; |
|
893 | 900 | height:8px; |
|
894 | 901 | font-size:0.8em; |
|
895 | 902 | color:#888; |
|
896 | 903 | padding:0; |
|
897 | 904 | margin:0; |
|
898 | 905 | line-height:16px; |
|
899 | 906 | white-space:nowrap; |
|
900 | 907 | } |
|
901 | 908 | |
|
902 | 909 | .task.label {width:100%;} |
|
903 | 910 | .task.label.project, .task.label.version { font-weight: bold; } |
|
904 | 911 | |
|
905 | 912 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } |
|
906 | 913 | .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } |
|
907 | 914 | .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } |
|
908 | 915 | |
|
909 | 916 | .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} |
|
910 | 917 | .task_late.parent, .task_done.parent { height: 3px;} |
|
911 | 918 | .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;} |
|
912 | 919 | .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;} |
|
913 | 920 | |
|
914 | 921 | .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
915 | 922 | .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
916 | 923 | .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
917 | 924 | .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } |
|
918 | 925 | |
|
919 | 926 | .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
920 | 927 | .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
921 | 928 | .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
922 | 929 | .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; } |
|
923 | 930 | |
|
924 | 931 | .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} |
|
925 | 932 | .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} |
|
926 | 933 | |
|
927 | 934 | /***** Icons *****/ |
|
928 | 935 | .icon { |
|
929 | 936 | background-position: 0% 50%; |
|
930 | 937 | background-repeat: no-repeat; |
|
931 | 938 | padding-left: 20px; |
|
932 | 939 | padding-top: 2px; |
|
933 | 940 | padding-bottom: 3px; |
|
934 | 941 | } |
|
935 | 942 | |
|
936 | 943 | .icon-add { background-image: url(../images/add.png); } |
|
937 | 944 | .icon-edit { background-image: url(../images/edit.png); } |
|
938 | 945 | .icon-copy { background-image: url(../images/copy.png); } |
|
939 | 946 | .icon-duplicate { background-image: url(../images/duplicate.png); } |
|
940 | 947 | .icon-del { background-image: url(../images/delete.png); } |
|
941 | 948 | .icon-move { background-image: url(../images/move.png); } |
|
942 | 949 | .icon-save { background-image: url(../images/save.png); } |
|
943 | 950 | .icon-cancel { background-image: url(../images/cancel.png); } |
|
944 | 951 | .icon-multiple { background-image: url(../images/table_multiple.png); } |
|
945 | 952 | .icon-folder { background-image: url(../images/folder.png); } |
|
946 | 953 | .open .icon-folder { background-image: url(../images/folder_open.png); } |
|
947 | 954 | .icon-package { background-image: url(../images/package.png); } |
|
948 | 955 | .icon-user { background-image: url(../images/user.png); } |
|
949 | 956 | .icon-projects { background-image: url(../images/projects.png); } |
|
950 | 957 | .icon-help { background-image: url(../images/help.png); } |
|
951 | 958 | .icon-attachment { background-image: url(../images/attachment.png); } |
|
952 | 959 | .icon-history { background-image: url(../images/history.png); } |
|
953 | 960 | .icon-time { background-image: url(../images/time.png); } |
|
954 | 961 | .icon-time-add { background-image: url(../images/time_add.png); } |
|
955 | 962 | .icon-stats { background-image: url(../images/stats.png); } |
|
956 | 963 | .icon-warning { background-image: url(../images/warning.png); } |
|
957 | 964 | .icon-fav { background-image: url(../images/fav.png); } |
|
958 | 965 | .icon-fav-off { background-image: url(../images/fav_off.png); } |
|
959 | 966 | .icon-reload { background-image: url(../images/reload.png); } |
|
960 | 967 | .icon-lock { background-image: url(../images/locked.png); } |
|
961 | 968 | .icon-unlock { background-image: url(../images/unlock.png); } |
|
962 | 969 | .icon-checked { background-image: url(../images/true.png); } |
|
963 | 970 | .icon-details { background-image: url(../images/zoom_in.png); } |
|
964 | 971 | .icon-report { background-image: url(../images/report.png); } |
|
965 | 972 | .icon-comment { background-image: url(../images/comment.png); } |
|
966 | 973 | .icon-summary { background-image: url(../images/lightning.png); } |
|
967 | 974 | .icon-server-authentication { background-image: url(../images/server_key.png); } |
|
968 | 975 | .icon-issue { background-image: url(../images/ticket.png); } |
|
969 | 976 | .icon-zoom-in { background-image: url(../images/zoom_in.png); } |
|
970 | 977 | .icon-zoom-out { background-image: url(../images/zoom_out.png); } |
|
971 | 978 | .icon-passwd { background-image: url(../images/textfield_key.png); } |
|
972 | 979 | .icon-test { background-image: url(../images/bullet_go.png); } |
|
973 | 980 | |
|
974 | 981 | .icon-file { background-image: url(../images/files/default.png); } |
|
975 | 982 | .icon-file.text-plain { background-image: url(../images/files/text.png); } |
|
976 | 983 | .icon-file.text-x-c { background-image: url(../images/files/c.png); } |
|
977 | 984 | .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); } |
|
978 | 985 | .icon-file.text-x-java { background-image: url(../images/files/java.png); } |
|
979 | 986 | .icon-file.text-x-javascript { background-image: url(../images/files/js.png); } |
|
980 | 987 | .icon-file.text-x-php { background-image: url(../images/files/php.png); } |
|
981 | 988 | .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); } |
|
982 | 989 | .icon-file.text-xml { background-image: url(../images/files/xml.png); } |
|
983 | 990 | .icon-file.text-css { background-image: url(../images/files/css.png); } |
|
984 | 991 | .icon-file.text-html { background-image: url(../images/files/html.png); } |
|
985 | 992 | .icon-file.image-gif { background-image: url(../images/files/image.png); } |
|
986 | 993 | .icon-file.image-jpeg { background-image: url(../images/files/image.png); } |
|
987 | 994 | .icon-file.image-png { background-image: url(../images/files/image.png); } |
|
988 | 995 | .icon-file.image-tiff { background-image: url(../images/files/image.png); } |
|
989 | 996 | .icon-file.application-pdf { background-image: url(../images/files/pdf.png); } |
|
990 | 997 | .icon-file.application-zip { background-image: url(../images/files/zip.png); } |
|
991 | 998 | .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); } |
|
992 | 999 | |
|
993 | 1000 | img.gravatar { |
|
994 | 1001 | padding: 2px; |
|
995 | 1002 | border: solid 1px #d5d5d5; |
|
996 | 1003 | background: #fff; |
|
997 | 1004 | vertical-align: middle; |
|
998 | 1005 | } |
|
999 | 1006 | |
|
1000 | 1007 | div.issue img.gravatar { |
|
1001 | 1008 | float: left; |
|
1002 | 1009 | margin: 0 6px 0 0; |
|
1003 | 1010 | padding: 5px; |
|
1004 | 1011 | } |
|
1005 | 1012 | |
|
1006 | 1013 | div.issue table img.gravatar { |
|
1007 | 1014 | height: 14px; |
|
1008 | 1015 | width: 14px; |
|
1009 | 1016 | padding: 2px; |
|
1010 | 1017 | float: left; |
|
1011 | 1018 | margin: 0 0.5em 0 0; |
|
1012 | 1019 | } |
|
1013 | 1020 | |
|
1014 | 1021 | h2 img.gravatar {margin: -2px 4px -4px 0;} |
|
1015 | 1022 | h3 img.gravatar {margin: -4px 4px -4px 0;} |
|
1016 | 1023 | h4 img.gravatar {margin: -6px 4px -4px 0;} |
|
1017 | 1024 | td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;} |
|
1018 | 1025 | #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;} |
|
1019 | 1026 | /* Used on 12px Gravatar img tags without the icon background */ |
|
1020 | 1027 | .icon-gravatar {float: left; margin-right: 4px;} |
|
1021 | 1028 | |
|
1022 | 1029 | #activity dt, .journal {clear: left;} |
|
1023 | 1030 | |
|
1024 | 1031 | .journal-link {float: right;} |
|
1025 | 1032 | |
|
1026 | 1033 | h2 img { vertical-align:middle; } |
|
1027 | 1034 | |
|
1028 | 1035 | .hascontextmenu { cursor: context-menu; } |
|
1029 | 1036 | |
|
1030 | 1037 | /************* CodeRay styles *************/ |
|
1031 | 1038 | .syntaxhl div {display: inline;} |
|
1032 | 1039 | .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;} |
|
1033 | 1040 | .syntaxhl .code pre { overflow: auto } |
|
1034 | 1041 | .syntaxhl .debug { color: white !important; background: blue !important; } |
|
1035 | 1042 | |
|
1036 | 1043 | .syntaxhl .annotation { color:#007 } |
|
1037 | 1044 | .syntaxhl .attribute-name { color:#b48 } |
|
1038 | 1045 | .syntaxhl .attribute-value { color:#700 } |
|
1039 | 1046 | .syntaxhl .binary { color:#509 } |
|
1040 | 1047 | .syntaxhl .char .content { color:#D20 } |
|
1041 | 1048 | .syntaxhl .char .delimiter { color:#710 } |
|
1042 | 1049 | .syntaxhl .char { color:#D20 } |
|
1043 | 1050 | .syntaxhl .class { color:#258; font-weight:bold } |
|
1044 | 1051 | .syntaxhl .class-variable { color:#369 } |
|
1045 | 1052 | .syntaxhl .color { color:#0A0 } |
|
1046 | 1053 | .syntaxhl .comment { color:#385 } |
|
1047 | 1054 | .syntaxhl .comment .char { color:#385 } |
|
1048 | 1055 | .syntaxhl .comment .delimiter { color:#385 } |
|
1049 | 1056 | .syntaxhl .complex { color:#A08 } |
|
1050 | 1057 | .syntaxhl .constant { color:#258; font-weight:bold } |
|
1051 | 1058 | .syntaxhl .decorator { color:#B0B } |
|
1052 | 1059 | .syntaxhl .definition { color:#099; font-weight:bold } |
|
1053 | 1060 | .syntaxhl .delimiter { color:black } |
|
1054 | 1061 | .syntaxhl .directive { color:#088; font-weight:bold } |
|
1055 | 1062 | .syntaxhl .doc { color:#970 } |
|
1056 | 1063 | .syntaxhl .doc-string { color:#D42; font-weight:bold } |
|
1057 | 1064 | .syntaxhl .doctype { color:#34b } |
|
1058 | 1065 | .syntaxhl .entity { color:#800; font-weight:bold } |
|
1059 | 1066 | .syntaxhl .error { color:#F00; background-color:#FAA } |
|
1060 | 1067 | .syntaxhl .escape { color:#666 } |
|
1061 | 1068 | .syntaxhl .exception { color:#C00; font-weight:bold } |
|
1062 | 1069 | .syntaxhl .float { color:#06D } |
|
1063 | 1070 | .syntaxhl .function { color:#06B; font-weight:bold } |
|
1064 | 1071 | .syntaxhl .global-variable { color:#d70 } |
|
1065 | 1072 | .syntaxhl .hex { color:#02b } |
|
1066 | 1073 | .syntaxhl .imaginary { color:#f00 } |
|
1067 | 1074 | .syntaxhl .include { color:#B44; font-weight:bold } |
|
1068 | 1075 | .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black } |
|
1069 | 1076 | .syntaxhl .inline-delimiter { font-weight: bold; color: #666 } |
|
1070 | 1077 | .syntaxhl .instance-variable { color:#33B } |
|
1071 | 1078 | .syntaxhl .integer { color:#06D } |
|
1072 | 1079 | .syntaxhl .key .char { color: #60f } |
|
1073 | 1080 | .syntaxhl .key .delimiter { color: #404 } |
|
1074 | 1081 | .syntaxhl .key { color: #606 } |
|
1075 | 1082 | .syntaxhl .keyword { color:#939; font-weight:bold } |
|
1076 | 1083 | .syntaxhl .label { color:#970; font-weight:bold } |
|
1077 | 1084 | .syntaxhl .local-variable { color:#963 } |
|
1078 | 1085 | .syntaxhl .namespace { color:#707; font-weight:bold } |
|
1079 | 1086 | .syntaxhl .octal { color:#40E } |
|
1080 | 1087 | .syntaxhl .operator { } |
|
1081 | 1088 | .syntaxhl .predefined { color:#369; font-weight:bold } |
|
1082 | 1089 | .syntaxhl .predefined-constant { color:#069 } |
|
1083 | 1090 | .syntaxhl .predefined-type { color:#0a5; font-weight:bold } |
|
1084 | 1091 | .syntaxhl .preprocessor { color:#579 } |
|
1085 | 1092 | .syntaxhl .pseudo-class { color:#00C; font-weight:bold } |
|
1086 | 1093 | .syntaxhl .regexp .content { color:#808 } |
|
1087 | 1094 | .syntaxhl .regexp .delimiter { color:#404 } |
|
1088 | 1095 | .syntaxhl .regexp .modifier { color:#C2C } |
|
1089 | 1096 | .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); } |
|
1090 | 1097 | .syntaxhl .reserved { color:#080; font-weight:bold } |
|
1091 | 1098 | .syntaxhl .shell .content { color:#2B2 } |
|
1092 | 1099 | .syntaxhl .shell .delimiter { color:#161 } |
|
1093 | 1100 | .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); } |
|
1094 | 1101 | .syntaxhl .string .char { color: #46a } |
|
1095 | 1102 | .syntaxhl .string .content { color: #46a } |
|
1096 | 1103 | .syntaxhl .string .delimiter { color: #46a } |
|
1097 | 1104 | .syntaxhl .string .modifier { color: #46a } |
|
1098 | 1105 | .syntaxhl .symbol .content { color:#d33 } |
|
1099 | 1106 | .syntaxhl .symbol .delimiter { color:#d33 } |
|
1100 | 1107 | .syntaxhl .symbol { color:#d33 } |
|
1101 | 1108 | .syntaxhl .tag { color:#070 } |
|
1102 | 1109 | .syntaxhl .type { color:#339; font-weight:bold } |
|
1103 | 1110 | .syntaxhl .value { color: #088; } |
|
1104 | 1111 | .syntaxhl .variable { color:#037 } |
|
1105 | 1112 | |
|
1106 | 1113 | .syntaxhl .insert { background: hsla(120,100%,50%,0.12) } |
|
1107 | 1114 | .syntaxhl .delete { background: hsla(0,100%,50%,0.12) } |
|
1108 | 1115 | .syntaxhl .change { color: #bbf; background: #007; } |
|
1109 | 1116 | .syntaxhl .head { color: #f8f; background: #505 } |
|
1110 | 1117 | .syntaxhl .head .filename { color: white; } |
|
1111 | 1118 | |
|
1112 | 1119 | .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; } |
|
1113 | 1120 | .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; } |
|
1114 | 1121 | |
|
1115 | 1122 | .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold } |
|
1116 | 1123 | .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold } |
|
1117 | 1124 | .syntaxhl .change .change { color: #88f } |
|
1118 | 1125 | .syntaxhl .head .head { color: #f4f } |
|
1119 | 1126 | |
|
1120 | 1127 | /***** Media print specific styles *****/ |
|
1121 | 1128 | @media print { |
|
1122 | 1129 | #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } |
|
1123 | 1130 | #main { background: #fff; } |
|
1124 | 1131 | #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} |
|
1125 | 1132 | #wiki_add_attachment { display:none; } |
|
1126 | 1133 | .hide-when-print { display: none; } |
|
1127 | 1134 | .autoscroll {overflow-x: visible;} |
|
1128 | 1135 | table.list {margin-top:0.5em;} |
|
1129 | 1136 | table.list th, table.list td {border: 1px solid #aaa;} |
|
1130 | 1137 | } |
|
1131 | 1138 | |
|
1132 | 1139 | /* Accessibility specific styles */ |
|
1133 | 1140 | .hidden-for-sighted { |
|
1134 | 1141 | position:absolute; |
|
1135 | 1142 | left:-10000px; |
|
1136 | 1143 | top:auto; |
|
1137 | 1144 | width:1px; |
|
1138 | 1145 | height:1px; |
|
1139 | 1146 | overflow:hidden; |
|
1140 | 1147 | } |
@@ -1,376 +1,385 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require File.expand_path('../../test_helper', __FILE__) |
|
21 | 21 | |
|
22 | 22 | class AttachmentsControllerTest < ActionController::TestCase |
|
23 | 23 | fixtures :users, :projects, :roles, :members, :member_roles, |
|
24 | 24 | :enabled_modules, :issues, :trackers, :attachments, |
|
25 | 25 | :versions, :wiki_pages, :wikis, :documents |
|
26 | 26 | |
|
27 | 27 | def setup |
|
28 | 28 | User.current = nil |
|
29 | 29 | set_fixtures_attachments_directory |
|
30 | 30 | end |
|
31 | 31 | |
|
32 | 32 | def teardown |
|
33 | 33 | set_tmp_attachments_directory |
|
34 | 34 | end |
|
35 | 35 | |
|
36 | 36 | def test_show_diff |
|
37 | 37 | ['inline', 'sbs'].each do |dt| |
|
38 | 38 | # 060719210727_changeset_utf8.diff |
|
39 | 39 | get :show, :id => 14, :type => dt |
|
40 | 40 | assert_response :success |
|
41 | 41 | assert_template 'diff' |
|
42 | 42 | assert_equal 'text/html', @response.content_type |
|
43 | 43 | assert_tag 'th', |
|
44 | 44 | :attributes => {:class => /filename/}, |
|
45 | 45 | :content => /issues_controller.rb\t\(rΓ©vision 1484\)/ |
|
46 | 46 | assert_tag 'td', |
|
47 | 47 | :attributes => {:class => /line-code/}, |
|
48 | 48 | :content => /Demande créée avec succès/ |
|
49 | 49 | end |
|
50 | 50 | set_tmp_attachments_directory |
|
51 | 51 | end |
|
52 | 52 | |
|
53 | 53 | def test_show_diff_replcace_cannot_convert_content |
|
54 | 54 | with_settings :repositories_encodings => 'UTF-8' do |
|
55 | 55 | ['inline', 'sbs'].each do |dt| |
|
56 | 56 | # 060719210727_changeset_iso8859-1.diff |
|
57 | 57 | get :show, :id => 5, :type => dt |
|
58 | 58 | assert_response :success |
|
59 | 59 | assert_template 'diff' |
|
60 | 60 | assert_equal 'text/html', @response.content_type |
|
61 | 61 | assert_tag 'th', |
|
62 | 62 | :attributes => {:class => "filename"}, |
|
63 | 63 | :content => /issues_controller.rb\t\(r\?vision 1484\)/ |
|
64 | 64 | assert_tag 'td', |
|
65 | 65 | :attributes => {:class => /line-code/}, |
|
66 | 66 | :content => /Demande cr\?\?e avec succ\?s/ |
|
67 | 67 | end |
|
68 | 68 | end |
|
69 | 69 | set_tmp_attachments_directory |
|
70 | 70 | end |
|
71 | 71 | |
|
72 | 72 | def test_show_diff_latin_1 |
|
73 | 73 | with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do |
|
74 | 74 | ['inline', 'sbs'].each do |dt| |
|
75 | 75 | # 060719210727_changeset_iso8859-1.diff |
|
76 | 76 | get :show, :id => 5, :type => dt |
|
77 | 77 | assert_response :success |
|
78 | 78 | assert_template 'diff' |
|
79 | 79 | assert_equal 'text/html', @response.content_type |
|
80 | 80 | assert_tag 'th', |
|
81 | 81 | :attributes => {:class => "filename"}, |
|
82 | 82 | :content => /issues_controller.rb\t\(rΓ©vision 1484\)/ |
|
83 | 83 | assert_tag 'td', |
|
84 | 84 | :attributes => {:class => /line-code/}, |
|
85 | 85 | :content => /Demande créée avec succès/ |
|
86 | 86 | end |
|
87 | 87 | end |
|
88 | 88 | set_tmp_attachments_directory |
|
89 | 89 | end |
|
90 | 90 | |
|
91 | 91 | def test_save_diff_type |
|
92 | 92 | user1 = User.find(1) |
|
93 | 93 | user1.pref[:diff_type] = nil |
|
94 | 94 | user1.preference.save |
|
95 | 95 | user = User.find(1) |
|
96 | 96 | assert_nil user.pref[:diff_type] |
|
97 | 97 | |
|
98 | 98 | @request.session[:user_id] = 1 # admin |
|
99 | 99 | get :show, :id => 5 |
|
100 | 100 | assert_response :success |
|
101 | 101 | assert_template 'diff' |
|
102 | 102 | user.reload |
|
103 | 103 | assert_equal "inline", user.pref[:diff_type] |
|
104 | 104 | get :show, :id => 5, :type => 'sbs' |
|
105 | 105 | assert_response :success |
|
106 | 106 | assert_template 'diff' |
|
107 | 107 | user.reload |
|
108 | 108 | assert_equal "sbs", user.pref[:diff_type] |
|
109 | 109 | end |
|
110 | 110 | |
|
111 | 111 | def test_diff_show_filename_in_mercurial_export |
|
112 | 112 | set_tmp_attachments_directory |
|
113 | 113 | a = Attachment.new(:container => Issue.find(1), |
|
114 | 114 | :file => uploaded_test_file("hg-export.diff", "text/plain"), |
|
115 | 115 | :author => User.find(1)) |
|
116 | 116 | assert a.save |
|
117 | 117 | assert_equal 'hg-export.diff', a.filename |
|
118 | 118 | |
|
119 | 119 | get :show, :id => a.id, :type => 'inline' |
|
120 | 120 | assert_response :success |
|
121 | 121 | assert_template 'diff' |
|
122 | 122 | assert_equal 'text/html', @response.content_type |
|
123 | 123 | assert_select 'th.filename', :text => 'test1.txt' |
|
124 | 124 | end |
|
125 | 125 | |
|
126 | 126 | def test_show_text_file |
|
127 | 127 | get :show, :id => 4 |
|
128 | 128 | assert_response :success |
|
129 | 129 | assert_template 'file' |
|
130 | 130 | assert_equal 'text/html', @response.content_type |
|
131 | 131 | set_tmp_attachments_directory |
|
132 | 132 | end |
|
133 | 133 | |
|
134 | 134 | def test_show_text_file_utf_8 |
|
135 | 135 | set_tmp_attachments_directory |
|
136 | 136 | a = Attachment.new(:container => Issue.find(1), |
|
137 | 137 | :file => uploaded_test_file("japanese-utf-8.txt", "text/plain"), |
|
138 | 138 | :author => User.find(1)) |
|
139 | 139 | assert a.save |
|
140 | 140 | assert_equal 'japanese-utf-8.txt', a.filename |
|
141 | 141 | |
|
142 | 142 | str_japanese = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" |
|
143 | 143 | str_japanese.force_encoding('UTF-8') if str_japanese.respond_to?(:force_encoding) |
|
144 | 144 | |
|
145 | 145 | get :show, :id => a.id |
|
146 | 146 | assert_response :success |
|
147 | 147 | assert_template 'file' |
|
148 | 148 | assert_equal 'text/html', @response.content_type |
|
149 | 149 | assert_tag :tag => 'th', |
|
150 | 150 | :content => '1', |
|
151 | 151 | :attributes => { :class => 'line-num' }, |
|
152 | 152 | :sibling => { :tag => 'td', :content => /#{str_japanese}/ } |
|
153 | 153 | end |
|
154 | 154 | |
|
155 | 155 | def test_show_text_file_replcace_cannot_convert_content |
|
156 | 156 | set_tmp_attachments_directory |
|
157 | 157 | with_settings :repositories_encodings => 'UTF-8' do |
|
158 | 158 | a = Attachment.new(:container => Issue.find(1), |
|
159 | 159 | :file => uploaded_test_file("iso8859-1.txt", "text/plain"), |
|
160 | 160 | :author => User.find(1)) |
|
161 | 161 | assert a.save |
|
162 | 162 | assert_equal 'iso8859-1.txt', a.filename |
|
163 | 163 | |
|
164 | 164 | get :show, :id => a.id |
|
165 | 165 | assert_response :success |
|
166 | 166 | assert_template 'file' |
|
167 | 167 | assert_equal 'text/html', @response.content_type |
|
168 | 168 | assert_tag :tag => 'th', |
|
169 | 169 | :content => '7', |
|
170 | 170 | :attributes => { :class => 'line-num' }, |
|
171 | 171 | :sibling => { :tag => 'td', :content => /Demande cr\?\?e avec succ\?s/ } |
|
172 | 172 | end |
|
173 | 173 | end |
|
174 | 174 | |
|
175 | 175 | def test_show_text_file_latin_1 |
|
176 | 176 | set_tmp_attachments_directory |
|
177 | 177 | with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do |
|
178 | 178 | a = Attachment.new(:container => Issue.find(1), |
|
179 | 179 | :file => uploaded_test_file("iso8859-1.txt", "text/plain"), |
|
180 | 180 | :author => User.find(1)) |
|
181 | 181 | assert a.save |
|
182 | 182 | assert_equal 'iso8859-1.txt', a.filename |
|
183 | 183 | |
|
184 | 184 | get :show, :id => a.id |
|
185 | 185 | assert_response :success |
|
186 | 186 | assert_template 'file' |
|
187 | 187 | assert_equal 'text/html', @response.content_type |
|
188 | 188 | assert_tag :tag => 'th', |
|
189 | 189 | :content => '7', |
|
190 | 190 | :attributes => { :class => 'line-num' }, |
|
191 | 191 | :sibling => { :tag => 'td', :content => /Demande créée avec succès/ } |
|
192 | 192 | end |
|
193 | 193 | end |
|
194 | 194 | |
|
195 | 195 | def test_show_text_file_should_send_if_too_big |
|
196 | 196 | Setting.file_max_size_displayed = 512 |
|
197 | 197 | Attachment.find(4).update_attribute :filesize, 754.kilobyte |
|
198 | 198 | |
|
199 | 199 | get :show, :id => 4 |
|
200 | 200 | assert_response :success |
|
201 | 201 | assert_equal 'application/x-ruby', @response.content_type |
|
202 | 202 | set_tmp_attachments_directory |
|
203 | 203 | end |
|
204 | 204 | |
|
205 | 205 | def test_show_other |
|
206 | 206 | get :show, :id => 6 |
|
207 | 207 | assert_response :success |
|
208 | 208 | assert_equal 'application/octet-stream', @response.content_type |
|
209 | 209 | set_tmp_attachments_directory |
|
210 | 210 | end |
|
211 | 211 | |
|
212 | 212 | def test_show_file_from_private_issue_without_permission |
|
213 | 213 | get :show, :id => 15 |
|
214 | 214 | assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2F15' |
|
215 | 215 | set_tmp_attachments_directory |
|
216 | 216 | end |
|
217 | 217 | |
|
218 | 218 | def test_show_file_from_private_issue_with_permission |
|
219 | 219 | @request.session[:user_id] = 2 |
|
220 | 220 | get :show, :id => 15 |
|
221 | 221 | assert_response :success |
|
222 | 222 | assert_tag 'h2', :content => /private.diff/ |
|
223 | 223 | set_tmp_attachments_directory |
|
224 | 224 | end |
|
225 | 225 | |
|
226 |
def test_show_file_without_container_should_be_ |
|
|
226 | def test_show_file_without_container_should_be_allowed_to_author | |
|
227 | 227 | set_tmp_attachments_directory |
|
228 | 228 | attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) |
|
229 | 229 | |
|
230 | 230 | @request.session[:user_id] = 2 |
|
231 | 231 | get :show, :id => attachment.id |
|
232 | assert_response 200 | |
|
233 | end | |
|
234 | ||
|
235 | def test_show_file_without_container_should_be_allowed_to_author | |
|
236 | set_tmp_attachments_directory | |
|
237 | attachment = Attachment.create!(:file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 2) | |
|
238 | ||
|
239 | @request.session[:user_id] = 3 | |
|
240 | get :show, :id => attachment.id | |
|
232 | 241 | assert_response 403 |
|
233 | 242 | end |
|
234 | 243 | |
|
235 | 244 | def test_show_invalid_should_respond_with_404 |
|
236 | 245 | get :show, :id => 999 |
|
237 | 246 | assert_response 404 |
|
238 | 247 | end |
|
239 | 248 | |
|
240 | 249 | def test_download_text_file |
|
241 | 250 | get :download, :id => 4 |
|
242 | 251 | assert_response :success |
|
243 | 252 | assert_equal 'application/x-ruby', @response.content_type |
|
244 | 253 | set_tmp_attachments_directory |
|
245 | 254 | end |
|
246 | 255 | |
|
247 | 256 | def test_download_version_file_with_issue_tracking_disabled |
|
248 | 257 | Project.find(1).disable_module! :issue_tracking |
|
249 | 258 | get :download, :id => 9 |
|
250 | 259 | assert_response :success |
|
251 | 260 | end |
|
252 | 261 | |
|
253 | 262 | def test_download_should_assign_content_type_if_blank |
|
254 | 263 | Attachment.find(4).update_attribute(:content_type, '') |
|
255 | 264 | |
|
256 | 265 | get :download, :id => 4 |
|
257 | 266 | assert_response :success |
|
258 | 267 | assert_equal 'text/x-ruby', @response.content_type |
|
259 | 268 | set_tmp_attachments_directory |
|
260 | 269 | end |
|
261 | 270 | |
|
262 | 271 | def test_download_missing_file |
|
263 | 272 | get :download, :id => 2 |
|
264 | 273 | assert_response 404 |
|
265 | 274 | set_tmp_attachments_directory |
|
266 | 275 | end |
|
267 | 276 | |
|
268 | 277 | def test_download_should_be_denied_without_permission |
|
269 | 278 | get :download, :id => 7 |
|
270 | 279 | assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fdownload%2F7' |
|
271 | 280 | set_tmp_attachments_directory |
|
272 | 281 | end |
|
273 | 282 | |
|
274 | 283 | if convert_installed? |
|
275 | 284 | def test_thumbnail |
|
276 | 285 | Attachment.clear_thumbnails |
|
277 | 286 | @request.session[:user_id] = 2 |
|
278 | 287 | |
|
279 | 288 | get :thumbnail, :id => 16 |
|
280 | 289 | assert_response :success |
|
281 | 290 | assert_equal 'image/png', response.content_type |
|
282 | 291 | end |
|
283 | 292 | |
|
284 | 293 | def test_thumbnail_should_not_exceed_maximum_size |
|
285 | 294 | Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800} |
|
286 | 295 | |
|
287 | 296 | @request.session[:user_id] = 2 |
|
288 | 297 | get :thumbnail, :id => 16, :size => 2000 |
|
289 | 298 | end |
|
290 | 299 | |
|
291 | 300 | def test_thumbnail_should_round_size |
|
292 | 301 | Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250} |
|
293 | 302 | |
|
294 | 303 | @request.session[:user_id] = 2 |
|
295 | 304 | get :thumbnail, :id => 16, :size => 260 |
|
296 | 305 | end |
|
297 | 306 | |
|
298 | 307 | def test_thumbnail_should_return_404_for_non_image_attachment |
|
299 | 308 | @request.session[:user_id] = 2 |
|
300 | 309 | |
|
301 | 310 | get :thumbnail, :id => 15 |
|
302 | 311 | assert_response 404 |
|
303 | 312 | end |
|
304 | 313 | |
|
305 | 314 | def test_thumbnail_should_return_404_if_thumbnail_generation_failed |
|
306 | 315 | Attachment.any_instance.stubs(:thumbnail).returns(nil) |
|
307 | 316 | @request.session[:user_id] = 2 |
|
308 | 317 | |
|
309 | 318 | get :thumbnail, :id => 16 |
|
310 | 319 | assert_response 404 |
|
311 | 320 | end |
|
312 | 321 | |
|
313 | 322 | def test_thumbnail_should_be_denied_without_permission |
|
314 | 323 | get :thumbnail, :id => 16 |
|
315 | 324 | assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16' |
|
316 | 325 | end |
|
317 | 326 | else |
|
318 | 327 | puts '(ImageMagick convert not available)' |
|
319 | 328 | end |
|
320 | 329 | |
|
321 | 330 | def test_destroy_issue_attachment |
|
322 | 331 | set_tmp_attachments_directory |
|
323 | 332 | issue = Issue.find(3) |
|
324 | 333 | @request.session[:user_id] = 2 |
|
325 | 334 | |
|
326 | 335 | assert_difference 'issue.attachments.count', -1 do |
|
327 | 336 | assert_difference 'Journal.count' do |
|
328 | 337 | delete :destroy, :id => 1 |
|
329 | 338 | assert_redirected_to '/projects/ecookbook' |
|
330 | 339 | end |
|
331 | 340 | end |
|
332 | 341 | assert_nil Attachment.find_by_id(1) |
|
333 | 342 | j = Journal.first(:order => 'id DESC') |
|
334 | 343 | assert_equal issue, j.journalized |
|
335 | 344 | assert_equal 'attachment', j.details.first.property |
|
336 | 345 | assert_equal '1', j.details.first.prop_key |
|
337 | 346 | assert_equal 'error281.txt', j.details.first.old_value |
|
338 | 347 | assert_equal User.find(2), j.user |
|
339 | 348 | end |
|
340 | 349 | |
|
341 | 350 | def test_destroy_wiki_page_attachment |
|
342 | 351 | set_tmp_attachments_directory |
|
343 | 352 | @request.session[:user_id] = 2 |
|
344 | 353 | assert_difference 'Attachment.count', -1 do |
|
345 | 354 | delete :destroy, :id => 3 |
|
346 | 355 | assert_response 302 |
|
347 | 356 | end |
|
348 | 357 | end |
|
349 | 358 | |
|
350 | 359 | def test_destroy_project_attachment |
|
351 | 360 | set_tmp_attachments_directory |
|
352 | 361 | @request.session[:user_id] = 2 |
|
353 | 362 | assert_difference 'Attachment.count', -1 do |
|
354 | 363 | delete :destroy, :id => 8 |
|
355 | 364 | assert_response 302 |
|
356 | 365 | end |
|
357 | 366 | end |
|
358 | 367 | |
|
359 | 368 | def test_destroy_version_attachment |
|
360 | 369 | set_tmp_attachments_directory |
|
361 | 370 | @request.session[:user_id] = 2 |
|
362 | 371 | assert_difference 'Attachment.count', -1 do |
|
363 | 372 | delete :destroy, :id => 9 |
|
364 | 373 | assert_response 302 |
|
365 | 374 | end |
|
366 | 375 | end |
|
367 | 376 | |
|
368 | 377 | def test_destroy_without_permission |
|
369 | 378 | set_tmp_attachments_directory |
|
370 | 379 | assert_no_difference 'Attachment.count' do |
|
371 | 380 | delete :destroy, :id => 3 |
|
372 | 381 | end |
|
373 | 382 | assert_response 302 |
|
374 | 383 | assert Attachment.find_by_id(3) |
|
375 | 384 | end |
|
376 | 385 | end |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
@@ -1,263 +1,263 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | require File.expand_path('../../test_helper', __FILE__) |
|
19 | 19 | require 'issues_controller' |
|
20 | 20 | |
|
21 | 21 | class IssuesControllerTransactionTest < ActionController::TestCase |
|
22 | 22 | tests IssuesController |
|
23 | 23 | fixtures :projects, |
|
24 | 24 | :users, |
|
25 | 25 | :roles, |
|
26 | 26 | :members, |
|
27 | 27 | :member_roles, |
|
28 | 28 | :issues, |
|
29 | 29 | :issue_statuses, |
|
30 | 30 | :versions, |
|
31 | 31 | :trackers, |
|
32 | 32 | :projects_trackers, |
|
33 | 33 | :issue_categories, |
|
34 | 34 | :enabled_modules, |
|
35 | 35 | :enumerations, |
|
36 | 36 | :attachments, |
|
37 | 37 | :workflows, |
|
38 | 38 | :custom_fields, |
|
39 | 39 | :custom_values, |
|
40 | 40 | :custom_fields_projects, |
|
41 | 41 | :custom_fields_trackers, |
|
42 | 42 | :time_entries, |
|
43 | 43 | :journals, |
|
44 | 44 | :journal_details, |
|
45 | 45 | :queries |
|
46 | 46 | |
|
47 | 47 | self.use_transactional_fixtures = false |
|
48 | 48 | |
|
49 | 49 | def setup |
|
50 | 50 | User.current = nil |
|
51 | 51 | end |
|
52 | 52 | |
|
53 | 53 | def test_update_stale_issue_should_not_update_the_issue |
|
54 | 54 | issue = Issue.find(2) |
|
55 | 55 | @request.session[:user_id] = 2 |
|
56 | 56 | |
|
57 | 57 | assert_no_difference 'Journal.count' do |
|
58 | 58 | assert_no_difference 'TimeEntry.count' do |
|
59 | 59 | put :update, |
|
60 | 60 | :id => issue.id, |
|
61 | 61 | :issue => { |
|
62 | 62 | :fixed_version_id => 4, |
|
63 | 63 | :notes => 'My notes', |
|
64 | 64 | :lock_version => (issue.lock_version - 1) |
|
65 | 65 | }, |
|
66 | 66 | :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } |
|
67 | 67 | end |
|
68 | 68 | end |
|
69 | 69 | |
|
70 | 70 | assert_response :success |
|
71 | 71 | assert_template 'edit' |
|
72 | 72 | |
|
73 | 73 | assert_select 'div.conflict' |
|
74 | 74 | assert_select 'input[name=?][value=?]', 'conflict_resolution', 'overwrite' |
|
75 | 75 | assert_select 'input[name=?][value=?]', 'conflict_resolution', 'add_notes' |
|
76 | 76 | assert_select 'label' do |
|
77 | 77 | assert_select 'input[name=?][value=?]', 'conflict_resolution', 'cancel' |
|
78 | 78 | assert_select 'a[href=/issues/2]' |
|
79 | 79 | end |
|
80 | 80 | end |
|
81 | 81 | |
|
82 | 82 | def test_update_stale_issue_should_save_attachments |
|
83 | 83 | set_tmp_attachments_directory |
|
84 | 84 | issue = Issue.find(2) |
|
85 | 85 | @request.session[:user_id] = 2 |
|
86 | 86 | |
|
87 | 87 | assert_no_difference 'Journal.count' do |
|
88 | 88 | assert_no_difference 'TimeEntry.count' do |
|
89 | 89 | assert_difference 'Attachment.count' do |
|
90 | 90 | put :update, |
|
91 | 91 | :id => issue.id, |
|
92 | 92 | :issue => { |
|
93 | 93 | :fixed_version_id => 4, |
|
94 | 94 | :notes => 'My notes', |
|
95 | 95 | :lock_version => (issue.lock_version - 1) |
|
96 | 96 | }, |
|
97 | 97 | :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}, |
|
98 | 98 | :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first.id } |
|
99 | 99 | end |
|
100 | 100 | end |
|
101 | 101 | end |
|
102 | 102 | |
|
103 | 103 | assert_response :success |
|
104 | 104 | assert_template 'edit' |
|
105 | 105 | attachment = Attachment.first(:order => 'id DESC') |
|
106 | 106 | assert_tag 'input', :attributes => {:name => 'attachments[p0][token]', :value => attachment.token} |
|
107 | assert_tag 'span', :content => /testfile.txt/ | |
|
107 | assert_tag 'input', :attributes => {:name => 'attachments[p0][filename]', :value => 'testfile.txt'} | |
|
108 | 108 | end |
|
109 | 109 | |
|
110 | 110 | def test_update_stale_issue_without_notes_should_not_show_add_notes_option |
|
111 | 111 | issue = Issue.find(2) |
|
112 | 112 | @request.session[:user_id] = 2 |
|
113 | 113 | |
|
114 | 114 | put :update, :id => issue.id, |
|
115 | 115 | :issue => { |
|
116 | 116 | :fixed_version_id => 4, |
|
117 | 117 | :notes => '', |
|
118 | 118 | :lock_version => (issue.lock_version - 1) |
|
119 | 119 | } |
|
120 | 120 | |
|
121 | 121 | assert_tag 'div', :attributes => {:class => 'conflict'} |
|
122 | 122 | assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'overwrite'} |
|
123 | 123 | assert_no_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'add_notes'} |
|
124 | 124 | assert_tag 'input', :attributes => {:name => 'conflict_resolution', :value => 'cancel'} |
|
125 | 125 | end |
|
126 | 126 | |
|
127 | 127 | def test_update_stale_issue_should_show_conflicting_journals |
|
128 | 128 | @request.session[:user_id] = 2 |
|
129 | 129 | |
|
130 | 130 | put :update, :id => 1, |
|
131 | 131 | :issue => { |
|
132 | 132 | :fixed_version_id => 4, |
|
133 | 133 | :notes => '', |
|
134 | 134 | :lock_version => 2 |
|
135 | 135 | }, |
|
136 | 136 | :last_journal_id => 1 |
|
137 | 137 | |
|
138 | 138 | assert_not_nil assigns(:conflict_journals) |
|
139 | 139 | assert_equal 1, assigns(:conflict_journals).size |
|
140 | 140 | assert_equal 2, assigns(:conflict_journals).first.id |
|
141 | 141 | assert_tag 'div', :attributes => {:class => 'conflict'}, |
|
142 | 142 | :descendant => {:content => /Some notes with Redmine links/} |
|
143 | 143 | end |
|
144 | 144 | |
|
145 | 145 | def test_update_stale_issue_without_previous_journal_should_show_all_journals |
|
146 | 146 | @request.session[:user_id] = 2 |
|
147 | 147 | |
|
148 | 148 | put :update, :id => 1, |
|
149 | 149 | :issue => { |
|
150 | 150 | :fixed_version_id => 4, |
|
151 | 151 | :notes => '', |
|
152 | 152 | :lock_version => 2 |
|
153 | 153 | }, |
|
154 | 154 | :last_journal_id => '' |
|
155 | 155 | |
|
156 | 156 | assert_not_nil assigns(:conflict_journals) |
|
157 | 157 | assert_equal 2, assigns(:conflict_journals).size |
|
158 | 158 | assert_tag 'div', :attributes => {:class => 'conflict'}, |
|
159 | 159 | :descendant => {:content => /Some notes with Redmine links/} |
|
160 | 160 | assert_tag 'div', :attributes => {:class => 'conflict'}, |
|
161 | 161 | :descendant => {:content => /Journal notes/} |
|
162 | 162 | end |
|
163 | 163 | |
|
164 | 164 | def test_update_stale_issue_should_show_private_journals_with_permission_only |
|
165 | 165 | journal = Journal.create!(:journalized => Issue.find(1), :notes => 'Privates notes', :private_notes => true, :user_id => 1) |
|
166 | 166 | |
|
167 | 167 | @request.session[:user_id] = 2 |
|
168 | 168 | put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' |
|
169 | 169 | assert_include journal, assigns(:conflict_journals) |
|
170 | 170 | |
|
171 | 171 | Role.find(1).remove_permission! :view_private_notes |
|
172 | 172 | put :update, :id => 1, :issue => {:fixed_version_id => 4, :lock_version => 2}, :last_journal_id => '' |
|
173 | 173 | assert_not_include journal, assigns(:conflict_journals) |
|
174 | 174 | end |
|
175 | 175 | |
|
176 | 176 | def test_update_stale_issue_with_overwrite_conflict_resolution_should_update |
|
177 | 177 | @request.session[:user_id] = 2 |
|
178 | 178 | |
|
179 | 179 | assert_difference 'Journal.count' do |
|
180 | 180 | put :update, :id => 1, |
|
181 | 181 | :issue => { |
|
182 | 182 | :fixed_version_id => 4, |
|
183 | 183 | :notes => 'overwrite_conflict_resolution', |
|
184 | 184 | :lock_version => 2 |
|
185 | 185 | }, |
|
186 | 186 | :conflict_resolution => 'overwrite' |
|
187 | 187 | end |
|
188 | 188 | |
|
189 | 189 | assert_response 302 |
|
190 | 190 | issue = Issue.find(1) |
|
191 | 191 | assert_equal 4, issue.fixed_version_id |
|
192 | 192 | journal = Journal.first(:order => 'id DESC') |
|
193 | 193 | assert_equal 'overwrite_conflict_resolution', journal.notes |
|
194 | 194 | assert journal.details.any? |
|
195 | 195 | end |
|
196 | 196 | |
|
197 | 197 | def test_update_stale_issue_with_add_notes_conflict_resolution_should_update |
|
198 | 198 | @request.session[:user_id] = 2 |
|
199 | 199 | |
|
200 | 200 | assert_difference 'Journal.count' do |
|
201 | 201 | put :update, :id => 1, |
|
202 | 202 | :issue => { |
|
203 | 203 | :fixed_version_id => 4, |
|
204 | 204 | :notes => 'add_notes_conflict_resolution', |
|
205 | 205 | :lock_version => 2 |
|
206 | 206 | }, |
|
207 | 207 | :conflict_resolution => 'add_notes' |
|
208 | 208 | end |
|
209 | 209 | |
|
210 | 210 | assert_response 302 |
|
211 | 211 | issue = Issue.find(1) |
|
212 | 212 | assert_nil issue.fixed_version_id |
|
213 | 213 | journal = Journal.first(:order => 'id DESC') |
|
214 | 214 | assert_equal 'add_notes_conflict_resolution', journal.notes |
|
215 | 215 | assert journal.details.empty? |
|
216 | 216 | end |
|
217 | 217 | |
|
218 | 218 | def test_update_stale_issue_with_cancel_conflict_resolution_should_redirect_without_updating |
|
219 | 219 | @request.session[:user_id] = 2 |
|
220 | 220 | |
|
221 | 221 | assert_no_difference 'Journal.count' do |
|
222 | 222 | put :update, :id => 1, |
|
223 | 223 | :issue => { |
|
224 | 224 | :fixed_version_id => 4, |
|
225 | 225 | :notes => 'add_notes_conflict_resolution', |
|
226 | 226 | :lock_version => 2 |
|
227 | 227 | }, |
|
228 | 228 | :conflict_resolution => 'cancel' |
|
229 | 229 | end |
|
230 | 230 | |
|
231 | 231 | assert_redirected_to '/issues/1' |
|
232 | 232 | issue = Issue.find(1) |
|
233 | 233 | assert_nil issue.fixed_version_id |
|
234 | 234 | end |
|
235 | 235 | |
|
236 | 236 | def test_put_update_with_spent_time_and_failure_should_not_add_spent_time |
|
237 | 237 | @request.session[:user_id] = 2 |
|
238 | 238 | |
|
239 | 239 | assert_no_difference('TimeEntry.count') do |
|
240 | 240 | put :update, |
|
241 | 241 | :id => 1, |
|
242 | 242 | :issue => { :subject => '' }, |
|
243 | 243 | :time_entry => { :hours => '2.5', :comments => 'should not be added', :activity_id => TimeEntryActivity.first.id } |
|
244 | 244 | assert_response :success |
|
245 | 245 | end |
|
246 | 246 | |
|
247 | 247 | assert_select 'input[name=?][value=?]', 'time_entry[hours]', '2.5' |
|
248 | 248 | assert_select 'input[name=?][value=?]', 'time_entry[comments]', 'should not be added' |
|
249 | 249 | assert_select 'select[name=?]', 'time_entry[activity_id]' do |
|
250 | 250 | assert_select 'option[value=?][selected=selected]', TimeEntryActivity.first.id |
|
251 | 251 | end |
|
252 | 252 | end |
|
253 | 253 | |
|
254 | 254 | def test_index_should_rescue_invalid_sql_query |
|
255 | 255 | IssueQuery.any_instance.stubs(:statement).returns("INVALID STATEMENT") |
|
256 | 256 | |
|
257 | 257 | get :index |
|
258 | 258 | assert_response 500 |
|
259 | 259 | assert_tag 'p', :content => /An error occurred/ |
|
260 | 260 | assert_nil session[:query] |
|
261 | 261 | assert_nil session[:issues_index_sort] |
|
262 | 262 | end |
|
263 | 263 | end |
General Comments 0
You need to be logged in to leave comments.
Login now