radiant-clipped-extension 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +230 -0
  3. data/Rakefile +109 -0
  4. data/app/controllers/admin/assets_controller.rb +60 -0
  5. data/app/controllers/admin/page_attachments_controller.rb +18 -0
  6. data/app/helpers/admin/assets_helper.rb +16 -0
  7. data/app/models/asset.rb +224 -0
  8. data/app/models/asset_tags.rb +384 -0
  9. data/app/models/asset_type.rb +170 -0
  10. data/app/models/old_page_attachment.rb +26 -0
  11. data/app/models/page_attachment.rb +21 -0
  12. data/app/views/admin/assets/_asset.html.haml +12 -0
  13. data/app/views/admin/assets/_asset_table.html.haml +30 -0
  14. data/app/views/admin/assets/_errors.html.haml +3 -0
  15. data/app/views/admin/assets/_form.html.haml +20 -0
  16. data/app/views/admin/assets/_page_assets.html.haml +12 -0
  17. data/app/views/admin/assets/_search.html.haml +17 -0
  18. data/app/views/admin/assets/_search_results.html.haml +17 -0
  19. data/app/views/admin/assets/edit.html.haml +40 -0
  20. data/app/views/admin/assets/index.html.haml +19 -0
  21. data/app/views/admin/assets/new.html.haml +24 -0
  22. data/app/views/admin/assets/refresh.html.haml +14 -0
  23. data/app/views/admin/assets/remove.html.haml +16 -0
  24. data/app/views/admin/configuration/_edit.html.haml +8 -0
  25. data/app/views/admin/configuration/_show.html.haml +12 -0
  26. data/app/views/admin/page_attachments/_attachment.html.haml +25 -0
  27. data/app/views/admin/page_parts/_page_part.html.haml +21 -0
  28. data/app/views/admin/pages/_asset_popups.html.haml +42 -0
  29. data/app/views/admin/pages/_assets.html.haml +11 -0
  30. data/app/views/admin/removed/_assets_bucket.html.haml +8 -0
  31. data/app/views/admin/removed/_assets_container.html.haml +58 -0
  32. data/app/views/admin/removed/_bucket.html.haml +11 -0
  33. data/app/views/admin/removed/_bucket_asset.html.haml +9 -0
  34. data/app/views/admin/removed/_show_bucket_link.html.haml +4 -0
  35. data/app/views/admin/removed/_upload_to_page.html.haml +12 -0
  36. data/app/views/admin/removed/bucket/_iframe.html.haml +1 -0
  37. data/artwork/icons.png +0 -0
  38. data/clipped_extension.rb +89 -0
  39. data/config/initializers/interpolation.rb +6 -0
  40. data/config/initializers/processors.rb +43 -0
  41. data/config/initializers/radiant_config.rb +34 -0
  42. data/config/locales/en.yml +108 -0
  43. data/config/locales/nl.yml +105 -0
  44. data/config/routes.rb +8 -0
  45. data/cucumber.yml +1 -0
  46. data/db/migrate/001_create_assets.rb +12 -0
  47. data/db/migrate/002_create_paperclip_attributes.rb +13 -0
  48. data/db/migrate/003_create_user_observer.rb +13 -0
  49. data/db/migrate/004_create_page_attachments.rb +19 -0
  50. data/db/migrate/005_rename_users.rb +13 -0
  51. data/db/migrate/20090316132151_disable_file_types.rb +20 -0
  52. data/db/migrate/20110513205050_asset_uuid.rb +11 -0
  53. data/db/migrate/20110606111250_update_configuration.rb +28 -0
  54. data/db/migrate/20110609101438_dimensions.rb +13 -0
  55. data/features/support/env.rb +11 -0
  56. data/features/support/paths.rb +22 -0
  57. data/lib/clipped_admin_ui.rb +32 -0
  58. data/lib/page_asset_associations.rb +13 -0
  59. data/lib/paperclip/frame_grab.rb +73 -0
  60. data/lib/paperclip/geometry_transformation.rb +79 -0
  61. data/lib/radiant-clipped-extension.rb +2 -0
  62. data/lib/radiant-clipped-extension/version.rb +3 -0
  63. data/lib/tasks/clipped_extension_tasks.rake +126 -0
  64. data/lib/tasks/paperclip_tasks.rake +79 -0
  65. data/public/flash/ZeroClipboard.swf +0 -0
  66. data/public/images/admin/assets/add.png +0 -0
  67. data/public/images/admin/assets/archive_icon.png +0 -0
  68. data/public/images/admin/assets/audio_icon.png +0 -0
  69. data/public/images/admin/assets/audio_thumbnail.png +0 -0
  70. data/public/images/admin/assets/c_icon.png +0 -0
  71. data/public/images/admin/assets/copy.png +0 -0
  72. data/public/images/admin/assets/css_icon.png +0 -0
  73. data/public/images/admin/assets/database_icon.png +0 -0
  74. data/public/images/admin/assets/delete.png +0 -0
  75. data/public/images/admin/assets/document_icon.png +0 -0
  76. data/public/images/admin/assets/document_thumbnail.png +0 -0
  77. data/public/images/admin/assets/flash_icon.png +0 -0
  78. data/public/images/admin/assets/flash_thumbnail.png +0 -0
  79. data/public/images/admin/assets/font_icon.png +0 -0
  80. data/public/images/admin/assets/gzip_icon.png +0 -0
  81. data/public/images/admin/assets/html_icon.png +0 -0
  82. data/public/images/admin/assets/image_icon.png +0 -0
  83. data/public/images/admin/assets/image_thumbnail.png +0 -0
  84. data/public/images/admin/assets/java_icon.png +0 -0
  85. data/public/images/admin/assets/page_edit.png +0 -0
  86. data/public/images/admin/assets/perl_icon.png +0 -0
  87. data/public/images/admin/assets/php_icon.png +0 -0
  88. data/public/images/admin/assets/presentation_icon.png +0 -0
  89. data/public/images/admin/assets/python_icon.png +0 -0
  90. data/public/images/admin/assets/reorder_assets.png +0 -0
  91. data/public/images/admin/assets/ruby_icon.png +0 -0
  92. data/public/images/admin/assets/script_icon.png +0 -0
  93. data/public/images/admin/assets/spreadsheet_icon.png +0 -0
  94. data/public/images/admin/assets/tar_icon.png +0 -0
  95. data/public/images/admin/assets/unknown_icon.png +0 -0
  96. data/public/images/admin/assets/unknown_thumbnail.png +0 -0
  97. data/public/images/admin/assets/video_icon.png +0 -0
  98. data/public/images/admin/assets/video_thumbnail.png +0 -0
  99. data/public/images/admin/assets/xml_icon.png +0 -0
  100. data/public/images/admin/assets/zip_icon.png +0 -0
  101. data/public/javascripts/admin/assets.js +296 -0
  102. data/public/stylesheets/sass/admin/assets.sass +209 -0
  103. data/radiant-clipped-extension.gemspec +33 -0
  104. data/spec/controllers/admin/assets_controller_spec.rb +50 -0
  105. data/spec/controllers/admin/page_attachments_controller_spec.rb +50 -0
  106. data/spec/datasets/assets_dataset.rb +35 -0
  107. data/spec/fixtures/test.flv +0 -0
  108. data/spec/lib/asset_tags_spec.rb +101 -0
  109. data/spec/lib/frame_grab_spec.rb +17 -0
  110. data/spec/lib/geometry_transformation_spec.rb +63 -0
  111. data/spec/models/asset_spec.rb +72 -0
  112. data/spec/models/asset_type_spec.rb +63 -0
  113. data/spec/spec.opts +6 -0
  114. data/spec/spec_helper.rb +36 -0
  115. data/wireframes/edit-page-assets-2.bmml +453 -0
  116. data/wireframes/edit-page-assets-2.png +0 -0
  117. data/wireframes/edit-page-assets-3.bmml +454 -0
  118. data/wireframes/edit-page-assets-3.png +0 -0
  119. data/wireframes/edit-page-assets.bmml +433 -0
  120. data/wireframes/edit-page-assets.png +0 -0
  121. data/wireframes/edit-page.bmml +174 -0
  122. data/wireframes/edit-page.png +0 -0
  123. metadata +249 -0
@@ -0,0 +1,13 @@
1
+ module PageAssetAssociations
2
+
3
+ #TODO: Turn page_attachments into a generic, polymorphic asset-attachment mechanism
4
+
5
+ def self.included(base)
6
+ base.class_eval {
7
+ has_many :page_attachments, :order => "position ASC"
8
+ has_many :assets, :through => :page_attachments, :order => "page_attachments.position ASC"
9
+ accepts_nested_attributes_for :page_attachments, :allow_destroy => true
10
+ }
11
+ end
12
+
13
+ end
@@ -0,0 +1,73 @@
1
+ module Paperclip
2
+ class FrameGrab < Processor
3
+
4
+ attr_accessor :time_offset, :current_geometry, :target_geometry, :whiny, :current_format, :target_format
5
+
6
+ def initialize(file, options = {}, attachment = nil)
7
+ super
8
+ @file = file
9
+ @time_offset = options[:time_offset] || '-4'
10
+ geometry = options[:geometry]
11
+ unless geometry.blank?
12
+ @crop = geometry[-1,1] == '#'
13
+ @target_geometry = Geometry.parse(geometry)
14
+ @current_geometry = Geometry.parse(video_dimensions(file))
15
+ end
16
+ @current_format = File.extname(@file.path)
17
+ @target_format = options[:format] || 'jpg'
18
+ @basename = File.basename(@file.path, @current_format)
19
+ @whiny = options[:whiny].nil? ? true : options[:whiny]
20
+ end
21
+
22
+ def crop?
23
+ !!@crop
24
+ end
25
+
26
+ def make
27
+ src = @file
28
+ dst = Tempfile.new([ @basename, @target_format ].compact.join("."))
29
+ dst.binmode
30
+
31
+ begin
32
+ # grab frame at offset
33
+ cmd = %Q[-itsoffset #{time_offset} -i :source -y -vcodec mjpeg -vframes 1 -an -f rawvideo ]
34
+
35
+ # if scale-and-crop parameters can be calculated, we pipe to convert for resizing
36
+ if scale_and_crop = transformation_options
37
+ cmd << %{pipe: | convert #{scale_and_crop} - #{target_format}:- }
38
+
39
+ # otherwise we let ffmpeg resize the to the right size without preserving aspect ratio
40
+ else
41
+ cmd << %{-s #{target_geometry} pipe: }
42
+ end
43
+
44
+ # then pipe to composite to overlay video icon
45
+ cmd << %{| composite -gravity center :icon - :dest }
46
+
47
+ Paperclip.run('ffmpeg', cmd, :source => File.expand_path(src.path), :dest => File.expand_path(dst.path), :icon => AssetType.find(:video).icon_path, :swallow_stderr => false)
48
+ rescue PaperclipCommandLineError => e
49
+ raise PaperclipError, "There was an error processing the thumbnail for #{@basename}: #{e}" if whiny
50
+ end
51
+
52
+ dst
53
+ end
54
+
55
+ # get video dimensions in nasty hacky way
56
+ def video_dimensions(file)
57
+ dim = Paperclip.run('ffmpeg', '-i :source 2>&1', :source => File.expand_path(file.path), :expected_outcodes => [0,1], :swallow_stderr => false)
58
+ $1 if dim =~ /(\d+x\d+)/
59
+ end
60
+
61
+ # Duplicated from Thumbnail. We can't just subclass because of assumed compatibility with Geometry.from_file
62
+ def transformation_options
63
+ if current_geometry
64
+ scale, crop = current_geometry.transformation_to(target_geometry, crop?)
65
+ trans = []
66
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
67
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
68
+ trans.join(" ")
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,79 @@
1
+ module Paperclip
2
+
3
+ class TransformationError < PaperclipError
4
+ end
5
+
6
+ class Geometry
7
+
8
+ # Returns a new Geometry object with the the same dimensions as this but with no modifier.
9
+ def without_modifier
10
+ Geometry.new(self.width, self.height)
11
+ end
12
+
13
+ # Returns the dimensions that would result if a thumbnail was created by transforming this geometry into that geometry.
14
+ # Its purpose is to mimic imagemagick conversions. Used like so:
15
+ # file_geometry.transformed_by(style_geometry)
16
+ # it returns the size of the thumbnail image you would get by applying that rule.
17
+ # This saves us having to go back to the file, which is expensive with S3.
18
+ # We understand all the Imagemagick geometry arguments described at http://www.imagemagick.org/script/command-line-processing.php#geometry
19
+ # including both '^' and paperclip's own '#' modifier.
20
+ def transformed_by (other)
21
+ raise TransformationError, "geometry is not transformable without both width and height" if self.height == 0 or self.width == 0
22
+ other = Geometry.parse(other) unless other.is_a? Geometry
23
+ return other.without_modifier if self =~ other
24
+ case other.modifier
25
+ when '#', '!', '^'
26
+ other.without_modifier
27
+ when '>'
28
+ (other.width < self.width || other.height < self.height) ? scaled_to_fit(other) : self
29
+ when '<'
30
+ (other.width > self.width && other.height > self.height) ? scaled_to_fit(other) : self
31
+ when '%'
32
+ scaled_by(other)
33
+ when '@'
34
+ scaled_by(other.width * 100 / (self.width * self.height))
35
+ else
36
+ scaled_to_fit(other)
37
+ end
38
+ end
39
+ alias :* :transformed_by
40
+
41
+ # Tests whether two geometries are identical in dimensions and modifier.
42
+ def == (other)
43
+ self.to_s == other.to_s
44
+ end
45
+
46
+ # Tests whether two geometries have the same dimensions, ignoring modifier.
47
+ def =~ (other)
48
+ self.height.to_i == other.height.to_i && self.width.to_i == other.width.to_i
49
+ end
50
+
51
+ # Scales this geometry to fit within that geometry.
52
+ def scaled_to_fit(other)
53
+ if (other.width > 0 && other.height == 0)
54
+ Geometry.new(other.width, self.height * other.width / self.width)
55
+ elsif (other.width == 0 && other.height > 0)
56
+ Geometry.new(self.width * other.height / self.height, other.height)
57
+ else
58
+ ratio = Geometry.new( other.width / self.width, other.height / self.height )
59
+ if ratio.square?
60
+ other.without_modifier
61
+ elsif ratio.horizontal?
62
+ Geometry.new(ratio.height * self.width, other.height)
63
+ else
64
+ Geometry.new(other.width, ratio.width * self.height)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Scales this geometry by the percentage(s) specified in that geometry.
70
+ def scaled_by(other)
71
+ other = Geometry.new("#{other}%") unless other.is_a? Geometry
72
+ if other.height > 0
73
+ Geometry.new(self.width * other.width / 100, self.height * other.height / 100)
74
+ else
75
+ Geometry.new(self.width * other.width / 100, self.height * other.width / 100)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,2 @@
1
+ module RadiantClippedExtension
2
+ end
@@ -0,0 +1,3 @@
1
+ module RadiantClippedExtension
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,126 @@
1
+ namespace :radiant do
2
+ namespace :extensions do
3
+ namespace :clipped do
4
+
5
+ desc "Runs the migration of the Clipped extension"
6
+ task :migrate => :environment do
7
+ require 'radiant/extension_migrator'
8
+ if ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name} WHERE version = 'Assets-20110513205050'").any?
9
+ puts "Assimilating Assets extension migration 20110513205050"
10
+ ClippedExtension.migrator.new(:up, ClippedExtension.migrations_path).send(:assume_migrated_upto_version, '20110513205050')
11
+ end
12
+
13
+ if ENV["VERSION"]
14
+ ClippedExtension.migrator.migrate(ENV["VERSION"].to_i)
15
+ Rake::Task['db:schema:dump'].invoke
16
+ else
17
+ ClippedExtension.migrator.migrate
18
+ Rake::Task['db:schema:dump'].invoke
19
+ end
20
+ end
21
+
22
+ desc "Copies public assets of the Clipped extension to the instance public/ directory."
23
+ task :update => :environment do
24
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
25
+ puts "Copying assets from ClippedExtension"
26
+ Dir[ClippedExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
27
+ path = file.sub(ClippedExtension.root, '')
28
+ directory = File.dirname(path)
29
+ mkdir_p RAILS_ROOT + directory, :verbose => false
30
+ cp_r file, RAILS_ROOT + path, :verbose => false
31
+ end
32
+
33
+ # unless ClippedExtension.starts_with? RAILS_ROOT # don't need to copy vendored tasks
34
+ # puts "Copying rake tasks from ClippedExtension"
35
+ # local_tasks_path = File.join(RAILS_ROOT, %w(lib tasks))
36
+ # mkdir_p local_tasks_path, :verbose => false
37
+ # Dir[File.join ClippedExtension.root, %w(lib tasks *.rake)].each do |file|
38
+ # cp file, local_tasks_path, :verbose => false
39
+ # end
40
+ # end
41
+
42
+ desc "Syncs all available translations for this ext to the English ext master"
43
+ task :sync => :environment do
44
+ # The main translation root, basically where English is kept
45
+ language_root = ClippedExtension.get_translation_keys(language_root)
46
+
47
+ Dir["#{language_root}/*.yml"].each do |filename|
48
+ next if filename.match('_available_tags')
49
+ basename = File.basename(filename, '.yml')
50
+ puts "Syncing #{basename}"
51
+ (comments, other) = TranslationSupport.read_file(filename, basename)
52
+ words.each { |k,v| other[k] ||= words[k] } # Initializing hash variable as empty if it does not exist
53
+ other.delete_if { |k,v| !words[k] } # Remove if not defined in en.yml
54
+ TranslationSupport.write_file(filename, basename, comments, other)
55
+ end
56
+ end
57
+ end
58
+
59
+ desc "Exports assets from database to assets directory"
60
+ task :export => :environment do
61
+ asset_path = File.join(RAILS_ROOT, "assets")
62
+ mkdir_p asset_path
63
+ Asset.find(:all).each do |asset|
64
+ puts "Exporting #{asset.asset_file_name}"
65
+ cp asset.asset.path, File.join(asset_path, asset.asset_file_name)
66
+ end
67
+ puts "Done."
68
+ end
69
+
70
+ desc "Imports assets to database from assets directory"
71
+ task :import => :environment do
72
+ asset_path = File.join(RAILS_ROOT, "assets")
73
+ if File.exist?(asset_path) && File.stat(asset_path).directory?
74
+ Dir.glob("#{asset_path}/*").each do |file_with_path|
75
+ if File.stat(file_with_path).file?
76
+ new_asset = File.new(file_with_path)
77
+ puts "Creating #{File.basename(file_with_path)}"
78
+ Asset.create :asset => new_asset
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ desc "Migrates page attachments from the original page attachments extension into new Assets"
85
+ task :migrate_from_page_attachments => :environment do
86
+ puts "This task can clean up traces of the page_attachments (think table records and files currently in /public/page_attachments).
87
+ If you would like to use this mode type \"yes\", type \"no\" or just hit enter to leave them untouched for now."
88
+ answer = STDIN.gets.chomp
89
+ erase_tracks = answer.eql?('yes') ? true : false
90
+ OldPageAttachment.find_all_by_parent_id(nil).each do |opa|
91
+ asset = opa.create_paperclipped_record
92
+ # move the actual file
93
+ old_dir = "#{RAILS_ROOT}/public/page_attachments/0000/#{opa.id.to_s.rjust(4,'0')}"
94
+ new_dir = "#{RAILS_ROOT}/public/assets/#{asset.id}"
95
+ puts "Copying #{old_dir.gsub(RAILS_ROOT, '')}/#{opa.filename} to #{new_dir.gsub(RAILS_ROOT, '')}/#{opa.filename}..."
96
+ mkdir_p new_dir
97
+ cp old_dir + "/#{opa.filename}", new_dir + "/#{opa.filename}"
98
+ # remove old record and remainings
99
+ if erase_tracks
100
+ rm_rf old_dir
101
+ end
102
+ end
103
+ # regenerate thumbnails
104
+ puts "Regenerating asset thumbnails"
105
+ ENV['CLASS'] = 'Asset'
106
+ Rake::Task['paperclip:refresh'].invoke
107
+ puts "Done."
108
+ end
109
+
110
+ desc "Migrates from old 'assets' extension."
111
+ task :migrate_from_assets => :environment do
112
+ Asset.delete_all("thumbnail IS NOT NULL OR parent_id IS NOT NULL")
113
+ ActiveRecord::Base.connection.tap do |c|
114
+ c.rename_column :assets, :filename, :asset_file_name
115
+ c.rename_column :assets, :content_type, :asset_content_type
116
+ c.rename_column :assets, :size, :asset_file_size
117
+ c.remove_column :assets, :parent_id
118
+ c.remove_column :assets, :thumbnail
119
+ end
120
+
121
+ ClippedExtension.migrator.new(:up, ClippedExtension.migrations_path).send(:assume_migrated_upto_version, 3)
122
+ ClippedExtension.migrator.migrate
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,79 @@
1
+ def obtain_class
2
+ class_name = ENV['CLASS'] || ENV['class']
3
+ raise "Must specify CLASS" unless class_name
4
+ @klass = Object.const_get(class_name)
5
+ end
6
+
7
+ def obtain_attachments
8
+ name = ENV['ATTACHMENT'] || ENV['attachment']
9
+ raise "Class #{@klass.name} has no attachments specified" unless @klass.respond_to?(:attachment_definitions)
10
+ if !name.blank? && @klass.attachment_definitions.keys.include?(name)
11
+ [ name ]
12
+ else
13
+ @klass.attachment_definitions.keys
14
+ end
15
+ end
16
+
17
+ def for_all_attachments
18
+ klass = obtain_class
19
+ names = obtain_attachments
20
+ ids = klass.connection.select_values(klass.send(:construct_finder_sql, :select => 'id'))
21
+
22
+ ids.each do |id|
23
+ instance = klass.find(id)
24
+ names.each do |name|
25
+ result = if instance.send("#{ name }?")
26
+ yield(instance, name)
27
+ else
28
+ true
29
+ end
30
+ print result ? "." : "x"; $stdout.flush
31
+ end
32
+ end
33
+ puts " Done."
34
+ end
35
+
36
+ namespace :paperclip do
37
+ desc "Refreshes both metadata and thumbnails."
38
+ task :refresh => ["paperclip:refresh:metadata", "paperclip:refresh:thumbnails"]
39
+
40
+ namespace :refresh do
41
+ desc "Regenerates thumbnails for a given CLASS (and optional ATTACHMENT)."
42
+ task :thumbnails => :environment do
43
+ errors = []
44
+ for_all_attachments do |instance, name|
45
+ result = instance.send(name).reprocess!
46
+ errors << [instance.id, instance.errors] unless instance.errors.blank?
47
+ result
48
+ end
49
+ errors.each{|e| puts "#{e.first}: #{e.last.full_messages.inspect}" }
50
+ end
51
+
52
+ desc "Regenerates content_type/size metadata for a given CLASS (and optional ATTACHMENT)."
53
+ task :metadata => :environment do
54
+ for_all_attachments do |instance, name|
55
+ if file = instance.send(name).to_file
56
+ instance.send("#{name}_file_name=", instance.send("#{name}_file_name").strip)
57
+ instance.send("#{name}_content_type=", file.content_type.strip)
58
+ instance.send("#{name}_file_size=", file.size) if instance.respond_to?("#{name}_file_size")
59
+ instance.save(false)
60
+ else
61
+ true
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ desc "Cleans out invalid attachments. Useful after you've added new validations."
68
+ task :clean => :environment do
69
+ for_all_attachments do |instance, name|
70
+ instance.send(name).send(:validate)
71
+ if instance.send(name).valid?
72
+ true
73
+ else
74
+ instance.send("#{name}=", nil)
75
+ instance.save
76
+ end
77
+ end
78
+ end
79
+ end
Binary file
Binary file
Binary file