diff --git a/app/controllers/attachments_controller.rb b/app/controllers/attachments_controller.rb index edae9b0cc..74a11a1c7 100644 --- a/app/controllers/attachments_controller.rb +++ b/app/controllers/attachments_controller.rb @@ -61,7 +61,7 @@ class AttachmentsController < ApplicationController end def thumbnail - if @attachment.thumbnailable? && Setting.thumbnails_enabled? && thumbnail = @attachment.thumbnail + if @attachment.thumbnailable? && thumbnail = @attachment.thumbnail(:size => params[:size]) if stale?(:etag => thumbnail) send_file thumbnail, :filename => filename_for_content_disposition(@attachment.filename), diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 9aeee07d0..a1c232d16 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -171,9 +171,17 @@ class Attachment < ActiveRecord::Base # Returns the full path the attachment thumbnail, or nil # if the thumbnail cannot be generated. - def thumbnail + def thumbnail(options={}) if thumbnailable? && readable? - size = Setting.thumbnails_size.to_i + size = options[:size].to_i + if size > 0 + # Limit the number of thumbnails per image + size = (size / 50) * 50 + # Maximum thumbnail size + size = 800 if size > 800 + else + size = Setting.thumbnails_size.to_i + end size = 100 unless size > 0 target = File.join(self.class.thumbnails_storage_path, "#{id}_#{digest}_#{size}.thumb") diff --git a/config/routes.rb b/config/routes.rb index 87e656475..d904c9b9e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -264,7 +264,7 @@ RedmineApp::Application.routes.draw do match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get - match 'attachments/thumbnail/:id', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get + match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/ resources :attachments, :only => [:show, :destroy] resources :groups do diff --git a/lib/redmine/wiki_formatting/macros.rb b/lib/redmine/wiki_formatting/macros.rb index cd1d83596..81e5999f8 100644 --- a/lib/redmine/wiki_formatting/macros.rb +++ b/lib/redmine/wiki_formatting/macros.rb @@ -116,6 +116,24 @@ module Redmine @included_wiki_pages.pop out end + + desc "Displays a clickable thumbnail of an attached image. Examples:\n\n
{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}
" + macro :thumbnail do |obj, args| + args, options = extract_macro_options(args, :size, :title) + filename = args.first + raise 'Filename required' unless filename.present? + size = options[:size] + raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/) + size = size.to_i + size = nil unless size > 0 + if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename) + title = options[:title] || attachment.title + img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename) + link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title) + else + raise "Attachment #{filename} not found" + end + end end end end diff --git a/test/functional/attachments_controller_test.rb b/test/functional/attachments_controller_test.rb index 949ed3979..11c3faa95 100644 --- a/test/functional/attachments_controller_test.rb +++ b/test/functional/attachments_controller_test.rb @@ -262,43 +262,44 @@ class AttachmentsControllerTest < ActionController::TestCase def test_thumbnail Attachment.clear_thumbnails @request.session[:user_id] = 2 - with_settings :thumbnails_enabled => '1' do - get :thumbnail, :id => 16 - assert_response :success - assert_equal 'image/png', response.content_type - end + + get :thumbnail, :id => 16 + assert_response :success + assert_equal 'image/png', response.content_type + end + + def test_thumbnail_should_not_exceed_maximum_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 800} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 2000 + end + + def test_thumbnail_should_round_size + Redmine::Thumbnail.expects(:generate).with {|source, target, size| size == 250} + + @request.session[:user_id] = 2 + get :thumbnail, :id => 16, :size => 260 end def test_thumbnail_should_return_404_for_non_image_attachment @request.session[:user_id] = 2 - with_settings :thumbnails_enabled => '1' do - get :thumbnail, :id => 15 - assert_response 404 - end - end - def test_thumbnail_should_return_404_if_thumbnails_not_enabled - @request.session[:user_id] = 2 - with_settings :thumbnails_enabled => '0' do - get :thumbnail, :id => 16 - assert_response 404 - end + get :thumbnail, :id => 15 + assert_response 404 end def test_thumbnail_should_return_404_if_thumbnail_generation_failed Attachment.any_instance.stubs(:thumbnail).returns(nil) @request.session[:user_id] = 2 - with_settings :thumbnails_enabled => '1' do - get :thumbnail, :id => 16 - assert_response 404 - end + + get :thumbnail, :id => 16 + assert_response 404 end def test_thumbnail_should_be_denied_without_permission - with_settings :thumbnails_enabled => '1' do - get :thumbnail, :id => 16 - assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16' - end + get :thumbnail, :id => 16 + assert_redirected_to '/login?back_url=http%3A%2F%2Ftest.host%2Fattachments%2Fthumbnail%2F16' end else puts '(ImageMagick convert not available)' diff --git a/test/integration/routing/attachments_test.rb b/test/integration/routing/attachments_test.rb index ba4bb2c36..fc54dea41 100644 --- a/test/integration/routing/attachments_test.rb +++ b/test/integration/routing/attachments_test.rb @@ -49,6 +49,10 @@ class RoutingAttachmentsTest < ActionController::IntegrationTest { :method => 'get', :path => "/attachments/thumbnail/1" }, { :controller => 'attachments', :action => 'thumbnail', :id => '1' } ) + assert_routing( + { :method => 'get', :path => "/attachments/thumbnail/1/200" }, + { :controller => 'attachments', :action => 'thumbnail', :id => '1', :size => '200' } + ) assert_routing( { :method => 'delete', :path => "/attachments/1" }, { :controller => 'attachments', :action => 'destroy', :id => '1' } diff --git a/test/unit/lib/redmine/wiki_formatting/macros_test.rb b/test/unit/lib/redmine/wiki_formatting/macros_test.rb index abc880068..4bea9a57d 100644 --- a/test/unit/lib/redmine/wiki_formatting/macros_test.rb +++ b/test/unit/lib/redmine/wiki_formatting/macros_test.rb @@ -119,4 +119,24 @@ class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase def test_macro_child_pages_without_wiki_page_should_fail assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}") end + + def test_macro_thumbnail + assert_equal '

testfile.PNG

', + textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14)) + end + + def test_macro_thumbnail_with_size + assert_equal '

testfile.PNG

', + textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14)) + end + + def test_macro_thumbnail_with_title + assert_equal '

testfile.PNG

', + textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14)) + end + + def test_macro_thumbnail_with_invalid_filename_should_fail + assert_include 'test.png not found', + textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14)) + end end